├── .gitignore ├── README.md ├── mqa-identifier-python.py ├── mqa_identifier_python ├── __init__.py ├── flac.py └── mqa_identifier.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .venv 3 | venv/ 4 | .vscode 5 | .DS_Store 6 | .idea/ 7 | .python-version 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MQA-identifier-python 2 | 3 | An MQA (Studio, originalSampleRate) identifier for "lossless" flac files written in Python. 4 | 5 | ## About The Project 6 | 7 | This project is a port of the awesome C++ project [MQA_identifier](https://github.com/purpl3F0x/MQA_identifier) by 8 | [@purpl3F0x](https://github.com/purpl3F0x) and [mqaid](https://github.com/redsudo/mqaid) by 9 | [@redsudo](https://github.com/redsudo). 10 | 11 | ## Getting Started 12 | 13 | ### Prerequisites 14 | 15 | - [Python 3.6+](https://python.org/) 16 | 17 | ### Installation 18 | 19 | 1. Clone the repo 20 | 21 | ```sh 22 | git clone https://github.com/Dniel97/MQA-identifier-python.git && cd MQA-identifier-python 23 | ``` 24 | 25 | 2. Install the requirements 26 | 27 | ```sh 28 | pip3 install -r requirements.txt 29 | ``` 30 | 31 | ## Usage 32 | 33 | ``` 34 | Usage: mqa-identifier-python.py [OPTIONS] [PATHS]... 35 | 36 | Options: 37 | --fix-tags Adds all the required tags for MQA such as MQAENCODE, 38 | ENCODER and ORIGINALSAMPLERATE. 39 | -?, -h, --help Show this message and exit. 40 | ``` 41 | 42 | ### Usage example 43 | 44 | ```shell 45 | python3 mqa-identifier-python.py --fix-tags "path/to/flac/files" 46 | ``` 47 | 48 | ``` 49 | Found 11 FLAC files to check 50 | MQA files will be tagged, overwriting existing MQA tags! 51 | # Encoding Name 52 | 1 NOT MQA 22. letzter song.flac 53 | 2 NOT MQA 23. judy.flac 54 | 3 MQA Studio 96kHz 01. Algorithm.mqa.flac 55 | 4 MQA Studio 48kHz 02. The Dark Side.mqa.flac 56 | 5 MQA Studio 96kHz 03. Pressure.mqa.flac 57 | 6 MQA Studio 48kHz 04. Propaganda.mqa.flac 58 | 7 MQA Studio 96kHz 05. Break It to Me.mqa.flac 59 | 8 MQA Studio 96kHz 06. Something Human.mqa.flac 60 | 9 MQA Studio 96kHz 07. Thought Contagion.mqa.flac 61 | 10 MQA Studio 96kHz 08. Get up and Fight.mqa.flac 62 | 11 MQA Studio 44.1kHz 09. Blockades.mqa.flac 63 | ``` 64 | 65 | ## Contributing 66 | 67 | Pull requests are welcome. 68 | 69 | ## Related Projects 70 | 71 | - [MQA_identifier](https://github.com/purpl3F0x/MQA_identifier) (Core) 72 | - [mqaid](https://github.com/redsudo/mqaid) 73 | -------------------------------------------------------------------------------- /mqa-identifier-python.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from datetime import datetime 4 | from pathlib import Path 5 | from mutagen.flac import FLAC 6 | 7 | from mqa_identifier_python.mqa_identifier import MqaIdentifier 8 | 9 | ENCODER = 'MQAEncode v1.1, 2.4.0+0 (278f5dd), E24F1DE5-32F1-4930-8197-24954EB9D6F4' 10 | 11 | 12 | @click.command(name="mqa-identifier-python", short_help="Identifies MQA FLAC files from a folder or from a file", 13 | context_settings=dict(help_option_names=["-?", "-h", "--help"] 14 | )) 15 | @click.option("--fix-tags", type=bool, default=False, is_flag=True, 16 | help="Adds all the required tags for MQA such as MQAENCODE, ENCODER and ORIGINALSAMPLERATE.") 17 | @click.argument("paths", nargs=-1, type=click.Path()) 18 | def main(paths: list, fix_tags: bool): 19 | if fix_tags: 20 | print('MQA tags will be added, overwriting existing MQA tags!') 21 | 22 | # get all flac paths from arguments 23 | flac_paths = [] 24 | 25 | for path in paths: 26 | path = Path(path) 27 | if Path.is_dir(path): 28 | # search for flac files recursively 29 | flac_paths += sorted(Path(path).glob('**/*.flac')) 30 | elif str(path).endswith('.flac') and path.is_file(): 31 | flac_paths.append(path) 32 | 33 | if len(flac_paths) == 0: 34 | print('No FLAC files could be found!') 35 | return 36 | 37 | print(f'Found {len(flac_paths)} FLAC files to check') 38 | print('#\tEncoding\t\t\t\tName') 39 | for i, file_path in enumerate(flac_paths): 40 | # let's identify the MQA file 41 | mqa = MqaIdentifier(file_path) 42 | file_name = file_path.parts[-1] 43 | # python is dumb 44 | nt = '\t\t' 45 | 46 | # check if file is MQA 47 | if mqa.is_mqa: 48 | # beauty print MQA stuff 49 | print(f'{i + 1}\tMQA{" Studio" if mqa.is_mqa_studio else ""} {mqa.get_original_sample_rate()}kHz' 50 | f'{"" if mqa.is_mqa_studio else nt}\t\t{file_name}') 51 | 52 | if fix_tags: 53 | # adding all needed MQA tags to file with mutagen 54 | tagger = FLAC(file_path) 55 | 56 | # generate the tags with current date, time 57 | encoder_time = datetime.now().strftime("%b %d %Y %H:%M:%S") 58 | tags = { 59 | 'ENCODER': f'{ENCODER}, {encoder_time}', 60 | 'MQAENCODER': f'{ENCODER}, {encoder_time}', 61 | 'ORIGINALSAMPLERATE': str(mqa.original_sample_rate) 62 | } 63 | 64 | # add tags as VORBIS commit to the FLAC file 65 | for k, v in tags.items(): 66 | tagger[k] = v 67 | 68 | # saving the tags 69 | tagger.save() 70 | 71 | else: 72 | # too lazy to do it better 73 | print(f'{i + 1}\tNOT MQA\t\t\t\t\t{file_name}') 74 | 75 | 76 | if __name__ == '__main__': 77 | main() 78 | -------------------------------------------------------------------------------- /mqa_identifier_python/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dniel97/MQA-identifier-python/ff0c9f1824d471d95ee8faf88af19325dc617d90/mqa_identifier_python/__init__.py -------------------------------------------------------------------------------- /mqa_identifier_python/flac.py: -------------------------------------------------------------------------------- 1 | # 2 | # Simple FLAC decoder (Python) 3 | # 4 | # Copyright (c) 2017 Project Nayuki. (MIT License) 5 | # https://www.nayuki.io/page/simple-flac-implementation 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | # this software and associated documentation files (the "Software"), to deal in 9 | # the Software without restriction, including without limitation the rights to 10 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 11 | # the Software, and to permit persons to whom the Software is furnished to do so, 12 | # subject to the following conditions: 13 | # - The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # - The Software is provided "as is", without warranty of any kind, express or 16 | # implied, including but not limited to the warranties of merchantability, 17 | # fitness for a particular purpose and noninfringement. In no event shall the 18 | # authors or copyright holders be liable for any claim, damages or other 19 | # liability, whether in an action of contract, tort or otherwise, arising from, 20 | # out of or in connection with the Software or the use or other dealings in the 21 | # Software. 22 | # 23 | 24 | import struct, sys 25 | python3 = sys.version_info.major >= 3 26 | 27 | 28 | def main(argv): 29 | if len(argv) != 3: 30 | sys.exit("Usage: python " + argv[0] + " InFile.flac OutFile.wav") 31 | with BitInputStream(open(argv[1], "rb")) as inp: 32 | with open(argv[2], "wb") as out: 33 | decode_file(inp, out) 34 | 35 | 36 | def decode_file(inp, out, numsamples=None, seconds=None): 37 | # Handle FLAC header and metadata blocks 38 | if inp.read_uint(32) != 0x664C6143: 39 | raise ValueError("Invalid magic string") 40 | samplerate = None 41 | last = False 42 | while not last: 43 | last = inp.read_uint(1) != 0 44 | type = inp.read_uint(7) 45 | length = inp.read_uint(24) 46 | if type == 0: # Stream info block 47 | inp.read_uint(16) 48 | inp.read_uint(16) 49 | inp.read_uint(24) 50 | inp.read_uint(24) 51 | samplerate = inp.read_uint(20) 52 | if seconds: 53 | numsamples = seconds * samplerate 54 | numchannels = inp.read_uint(3) + 1 55 | sampledepth = inp.read_uint(5) + 1 56 | x = inp.read_uint(36) 57 | numsamples = numsamples or x 58 | inp.read_uint(128) 59 | else: 60 | for i in range(length): 61 | inp.read_uint(8) 62 | if samplerate is None: 63 | raise ValueError("Stream info metadata block absent") 64 | if sampledepth % 8 != 0: 65 | raise RuntimeError("Sample depth not supported") 66 | 67 | # Start writing WAV file headers 68 | sampledatalen = numsamples * numchannels * (sampledepth // 8) 69 | out.write(b"RIFF") 70 | out.write(struct.pack(" 0: 80 | numsamples -= decode_frame(inp, numchannels, sampledepth, out) 81 | 82 | 83 | def decode_frame(inp, numchannels, sampledepth, out): 84 | # Read a ton of header fields, and ignore most of them 85 | temp = inp.read_byte() 86 | if temp == -1: 87 | return False 88 | sync = temp << 6 | inp.read_uint(6) 89 | if sync != 0x3FFE: 90 | raise ValueError("Sync code expected") 91 | 92 | inp.read_uint(1) 93 | inp.read_uint(1) 94 | blocksizecode = inp.read_uint(4) 95 | sampleratecode = inp.read_uint(4) 96 | chanasgn = inp.read_uint(4) 97 | inp.read_uint(3) 98 | inp.read_uint(1) 99 | 100 | temp = inp.read_uint(8) 101 | while temp >= 0b11000000: 102 | inp.read_uint(8) 103 | temp = (temp << 1) & 0xFF 104 | 105 | if blocksizecode == 1: 106 | blocksize = 192 107 | elif 2 <= blocksizecode <= 5: 108 | blocksize = 576 << blocksizecode - 2 109 | elif blocksizecode == 6: 110 | blocksize = inp.read_uint(8) + 1 111 | elif blocksizecode == 7: 112 | blocksize = inp.read_uint(16) + 1 113 | elif 8 <= blocksizecode <= 15: 114 | blocksize = 256 << (blocksizecode - 8) 115 | 116 | if sampleratecode == 12: 117 | inp.read_uint(8) 118 | elif sampleratecode in (13, 14): 119 | inp.read_uint(16) 120 | 121 | inp.read_uint(8) 122 | 123 | # Decode each channel's subframe, then skip footer 124 | samples = decode_subframes(inp, blocksize, sampledepth, chanasgn) 125 | inp.align_to_byte() 126 | inp.read_uint(16) 127 | 128 | # Write the decoded samples 129 | numbytes = sampledepth // 8 130 | if python3: 131 | def write_little_int(val): 132 | out.write(bytes(((val >> (i * 8)) & 0xFF) for i in range(numbytes))) 133 | else: 134 | def write_little_int(val): 135 | out.write("".join(chr((val >> (i * 8)) & 0xFF) for i in range(numbytes))) 136 | addend = 128 if sampledepth == 8 else 0 137 | for i in range(blocksize): 138 | for j in range(numchannels): 139 | write_little_int(samples[j][i] + addend) 140 | return blocksize 141 | 142 | 143 | def decode_subframes(inp, blocksize, sampledepth, chanasgn): 144 | if 0 <= chanasgn <= 7: 145 | return [decode_subframe(inp, blocksize, sampledepth) for _ in range(chanasgn + 1)] 146 | elif 8 <= chanasgn <= 10: 147 | temp0 = decode_subframe(inp, blocksize, sampledepth + (1 if (chanasgn == 9) else 0)) 148 | temp1 = decode_subframe(inp, blocksize, sampledepth + (0 if (chanasgn == 9) else 1)) 149 | if chanasgn == 8: 150 | for i in range(blocksize): 151 | temp1[i] = temp0[i] - temp1[i] 152 | elif chanasgn == 9: 153 | for i in range(blocksize): 154 | temp0[i] += temp1[i] 155 | elif chanasgn == 10: 156 | for i in range(blocksize): 157 | side = temp1[i] 158 | right = temp0[i] - (side >> 1) 159 | temp1[i] = right 160 | temp0[i] = right + side 161 | return [temp0, temp1] 162 | else: 163 | raise ValueError("Reserved channel assignment") 164 | 165 | 166 | def decode_subframe(inp, blocksize, sampledepth): 167 | inp.read_uint(1) 168 | type = inp.read_uint(6) 169 | shift = inp.read_uint(1) 170 | if shift == 1: 171 | while inp.read_uint(1) == 0: 172 | shift += 1 173 | sampledepth -= shift 174 | 175 | if type == 0: # Constant coding 176 | result = [inp.read_signed_int(sampledepth)] * blocksize 177 | elif type == 1: # Verbatim coding 178 | result = [inp.read_signed_int(sampledepth) for _ in range(blocksize)] 179 | elif 8 <= type <= 12: 180 | result = decode_fixed_prediction_subframe(inp, type - 8, blocksize, sampledepth) 181 | elif 32 <= type <= 63: 182 | result = decode_linear_predictive_coding_subframe(inp, type - 31, blocksize, sampledepth) 183 | else: 184 | raise ValueError("Reserved subframe type") 185 | return [(v << shift) for v in result] 186 | 187 | 188 | def decode_fixed_prediction_subframe(inp, predorder, blocksize, sampledepth): 189 | result = [inp.read_signed_int(sampledepth) for _ in range(predorder)] 190 | decode_residuals(inp, blocksize, result) 191 | restore_linear_prediction(result, FIXED_PREDICTION_COEFFICIENTS[predorder], 0) 192 | return result 193 | 194 | FIXED_PREDICTION_COEFFICIENTS = ( 195 | (), 196 | (1,), 197 | (2, -1), 198 | (3, -3, 1), 199 | (4, -6, 4, -1), 200 | ) 201 | 202 | 203 | def decode_linear_predictive_coding_subframe(inp, lpcorder, blocksize, sampledepth): 204 | result = [inp.read_signed_int(sampledepth) for _ in range(lpcorder)] 205 | precision = inp.read_uint(4) + 1 206 | shift = inp.read_signed_int(5) 207 | coefs = [inp.read_signed_int(precision) for _ in range(lpcorder)] 208 | decode_residuals(inp, blocksize, result) 209 | restore_linear_prediction(result, coefs, shift) 210 | return result 211 | 212 | 213 | def decode_residuals(inp, blocksize, result): 214 | method = inp.read_uint(2) 215 | if method >= 2: 216 | raise ValueError("Reserved residual coding method") 217 | parambits = [4, 5][method] 218 | escapeparam = [0xF, 0x1F][method] 219 | 220 | partitionorder = inp.read_uint(4) 221 | numpartitions = 1 << partitionorder 222 | if blocksize % numpartitions != 0: 223 | raise ValueError("Block size not divisible by number of Rice partitions") 224 | 225 | for i in range(numpartitions): 226 | count = blocksize >> partitionorder 227 | if i == 0: 228 | count -= len(result) 229 | param = inp.read_uint(parambits) 230 | if param < escapeparam: 231 | result.extend(inp.read_rice_signed_int(param) for _ in range(count)) 232 | else: 233 | numbits = inp.read_uint(5) 234 | result.extend(inp.read_signed_int(numbits) for _ in range(count)) 235 | 236 | 237 | def restore_linear_prediction(result, coefs, shift): 238 | for i in range(len(coefs), len(result)): 239 | result[i] += sum((result[i - 1 - j] * c) for (j, c) in enumerate(coefs)) >> shift 240 | 241 | 242 | 243 | class BitInputStream(object): 244 | 245 | def __init__(self, inp): 246 | self.inp = inp 247 | self.bitbuffer = 0 248 | self.bitbufferlen = 0 249 | 250 | 251 | def align_to_byte(self): 252 | self.bitbufferlen -= self.bitbufferlen % 8 253 | 254 | 255 | def read_byte(self): 256 | if self.bitbufferlen >= 8: 257 | return self.read_uint(8) 258 | else: 259 | result = self.inp.read(1) 260 | if len(result) == 0: 261 | return -1 262 | return result[0] if python3 else ord(result) 263 | 264 | 265 | def read_uint(self, n): 266 | while self.bitbufferlen < n: 267 | temp = self.inp.read(1) 268 | if len(temp) == 0: 269 | raise EOFError() 270 | temp = temp[0] if python3 else ord(temp) 271 | self.bitbuffer = (self.bitbuffer << 8) | temp 272 | self.bitbufferlen += 8 273 | self.bitbufferlen -= n 274 | result = (self.bitbuffer >> self.bitbufferlen) & ((1 << n) - 1) 275 | self.bitbuffer &= (1 << self.bitbufferlen) - 1 276 | return result 277 | 278 | 279 | def read_signed_int(self, n): 280 | temp = self.read_uint(n) 281 | temp -= (temp >> (n - 1)) << n 282 | return temp 283 | 284 | 285 | def read_rice_signed_int(self, param): 286 | val = 0 287 | while self.read_uint(1) == 0: 288 | val += 1 289 | val = (val << param) | self.read_uint(param) 290 | return (val >> 1) ^ -(val & 1) 291 | 292 | 293 | def close(self): 294 | self.inp.close() 295 | 296 | 297 | def __enter__(self): 298 | return self 299 | 300 | 301 | def __exit__(self, type, value, traceback): 302 | self.close() 303 | 304 | 305 | 306 | if __name__ == "__main__": 307 | main(sys.argv) 308 | -------------------------------------------------------------------------------- /mqa_identifier_python/mqa_identifier.py: -------------------------------------------------------------------------------- 1 | import io 2 | import struct 3 | import wave 4 | import sys 5 | from pathlib import Path 6 | 7 | # needed for correct import 8 | sys.path.append('/'.join(__file__.replace('\\', '/').split('/')[:-1])) 9 | import flac 10 | 11 | 12 | def twos_complement(n, bits): 13 | mask = 2 ** (bits - 1) 14 | return -(n & mask) + (n & ~mask) 15 | 16 | 17 | def iter_i24_as_i32(data): 18 | for l, h in struct.iter_unpack(' int: 35 | """ 36 | Decodes from a 4 bit int the originalSampleRate: 37 | 0: 44100Hz 38 | 1: 48000Hz 39 | 4: 176400Hz 40 | 5: 192000Hz 41 | 8: 88200Hz 42 | 9: 96000Hz 43 | 12: 352800Hz 44 | 13: 384000Hz 45 | if LSB is 0 then base is 44100Hz else 48000Hz 46 | the first 3 MSBs need to be rotated and raised to the power of 2 (so 1, 2, 4, 8, ...) 47 | :param c: Is a 4 bit integer 48 | :return: The sample rate in Hz 49 | """ 50 | base = 48000 if (c & 1) == 1 else 44100 51 | # jesus @purpl3F0x 52 | multiplier = 1 << (((c >> 3) & 1) | (((c >> 2) & 1) << 1) | (((c >> 1) & 1) << 2)) 53 | 54 | return base * multiplier 55 | 56 | 57 | MAGIC = 51007556744 # int.from_bytes(bytes.fromhex('0be0498c88'), 'big') jesus christ 58 | 59 | 60 | class MqaIdentifier: 61 | def __init__(self, flac_file_path: str or Path): 62 | self.is_mqa = False 63 | self.is_mqa_studio = False 64 | self.original_sample_rate = None 65 | self.bit_depth = 16 66 | 67 | self.detect(flac_file_path) 68 | 69 | def get_original_sample_rate(self) -> float or int: 70 | """ 71 | Get the originalSampleRate in int or float depending on the frequency 72 | :return: sample rate in kHz 73 | """ 74 | sample_rate = self.original_sample_rate / 1000 75 | if sample_rate.is_integer(): 76 | return int(sample_rate) 77 | return sample_rate 78 | 79 | def _decode_flac_samples(self, flac_file_path: str or Path) -> list: 80 | """ 81 | Decodes a 16/24bit flac file to a samples list 82 | 83 | :param flac_file_path: Path to the flac file 84 | :return: Returns decoded samples in a list 85 | """ 86 | with open(str(flac_file_path), 'rb') as f: 87 | magic = peek(f, 4) 88 | 89 | if magic == b'fLaC': 90 | with flac.BitInputStream(f) as bf: 91 | f = io.BytesIO() 92 | # ignore EOFError 93 | try: 94 | flac.decode_file(bf, f, seconds=1) 95 | except EOFError: 96 | pass 97 | f.seek(0) 98 | 99 | with wave.open(f) as wf: 100 | channel_count, sample_width, framerate, *_ = wf.getparams() 101 | 102 | if channel_count != 2: 103 | raise ValueError('Input must be stereo') 104 | 105 | if sample_width == 3: 106 | iter_data = iter_i24_as_i32 107 | self.bit_depth = 24 108 | elif sample_width == 2: 109 | iter_data = iter_i16_as_i32 110 | else: 111 | raise ValueError('Input must be 16 or 24-bit') 112 | 113 | return list(iter_data(wf.readframes(framerate))) 114 | 115 | def detect(self, flac_file_path: str or Path) -> bool: 116 | """ 117 | Detects if the FLAC file is a MQA file and also detects if it's MQA Studio (blue) and the originalSampleRate 118 | 119 | :param flac_file_path: Path to the flac file 120 | :return: True if MQA got detected and False if not 121 | """ 122 | # get the samples from the FLAC decoder 123 | samples = self._decode_flac_samples(flac_file_path) 124 | # samples[::2] are left channel and samples[1::2] right channel samples 125 | channel_samples = list(zip(samples[::2], samples[1::2])) 126 | 127 | # dictionary to save all the buffers for 16, 17 and 18 bit shifts 128 | buffer = {16: 0, 17: 0, 18: 0} 129 | for i, sample in enumerate(channel_samples): 130 | # sample[0] is the left channel sample and sample[1] the right channel sample 131 | # perform a XOR with both samples and bitshift it by 16, 17 and 18 132 | buffer = {key: value | (sample[0] ^ sample[1]) >> key & 1 for key, value in buffer.items()} 133 | 134 | # int.from_bytes(bytes.fromhex('0be0498c88'), 'big') 135 | if MAGIC in buffer.values(): 136 | # found MQA sync word 137 | self.is_mqa = True 138 | 139 | # get the bitshift position where the MAGIC was found, ugly but works 140 | pos = [k for k, v in buffer.items() if v == MAGIC][0] 141 | 142 | # get originalSampleRate 143 | org = 0 144 | for k in range(3, 7): 145 | j = ((channel_samples[i + k][0]) ^ (channel_samples[i + k][1])) >> pos & 1 146 | org |= j << (6 - k) 147 | 148 | # decode the 4 bit int to the originalSampleRate 149 | self.original_sample_rate = original_sample_rate_decoder(org) 150 | 151 | # get MQA Studio 152 | provenance = 0 153 | for k in range(29, 34): 154 | j = ((channel_samples[i + k][0]) ^ (channel_samples[i + k][1])) >> pos & 1 155 | provenance |= j << (33 - k) 156 | 157 | # check if its MQA Studio (blue) 158 | self.is_mqa_studio = provenance > 8 159 | 160 | return True 161 | else: 162 | buffer = {key: (value << 1) & 0xFFFFFFFFF for key, value in buffer.items()} 163 | 164 | return False 165 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Used for argmunets 2 | click>=8.0.1 3 | 4 | # Used for adding MQA tags 5 | mutagen>=1.45.1 --------------------------------------------------------------------------------