├── requirements.txt ├── README.md ├── Pipfile ├── Pipfile.lock ├── LICENSE ├── .gitignore ├── is_mqa.py └── flac.py /requirements.txt: -------------------------------------------------------------------------------- 1 | bitstring==3.1.6 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mqaid 2 | An MQA identifier for "lossless" flac files. See https://en.wikipedia.org/wiki/Master_Quality_Authenticated 3 | 4 | Requires Python 3.5+ and module 'bitstring' 5 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | bitstring = "*" 10 | 11 | [requires] 12 | python_version = "3.7" 13 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "c7dc62406beff2cf6c04a7cf71187a34ffb3f2f14f9abc76a10614b4cf14ab31" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "bitstring": { 20 | "hashes": [ 21 | "sha256:7b60b0c300d0d3d0a24ec84abfda4b0eaed3dc56dc90f6cbfe497166c9ad8443", 22 | "sha256:c97a8e2a136e99b523b27da420736ae5cb68f83519d633794a6a11192f69f8bf", 23 | "sha256:e392819965e7e0246e3cf6a51d5a54e731890ae03ebbfa3cd0e4f74909072096" 24 | ], 25 | "index": "pypi", 26 | "version": "==3.1.6" 27 | } 28 | }, 29 | "develop": {} 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 redsudo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /is_mqa.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import sys 3 | import io 4 | import wave 5 | import flac 6 | from pathlib import Path 7 | from bitstring import Bits 8 | 9 | MAGIC = Bits('0xbe0498c88') 10 | 11 | def twos_complement(n, bits): 12 | mask = 2 ** (bits - 1) 13 | return -(n & mask) + (n & ~mask) 14 | 15 | def iter_i24_as_i32(data): 16 | for l, h in struct.iter_unpack('> p & 1 56 | for x, y in zip(samples[::2], samples[1::2])) 57 | for p in range(16, 24)) 58 | 59 | if any(s.find(MAGIC) for s in streams): 60 | print('\x1b[1;31m MQA syncword present. [{}] \x1b[0m'.format(str(path))) 61 | else: 62 | print('\x1b[1;32m Didn\'t find an MQA syncword. [{}] \x1b[0m'.format(path.parts[-1])) 63 | 64 | if __name__ == '__main__': 65 | args = sys.argv[1:] 66 | flacpaths = [] 67 | 68 | for path in args: 69 | path = Path(path) 70 | if Path.is_dir(path): 71 | flacpaths += sorted(Path(path).glob('**/*.flac')) 72 | elif str(path).endswith('.flac') and path.is_file(): 73 | flacpaths.append(path) 74 | 75 | print('\x1b[1;33m Found {} flac file(s). Decoding now... \x1b[0m'.format(len(flacpaths))) 76 | for fpath in flacpaths: 77 | try: 78 | main(fpath) 79 | except Exception as ex: 80 | print(ex) 81 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------