|
| 1 | +<!DOCTYPE html> |
| 2 | +<html> |
| 3 | +<head> |
| 4 | + <style> |
| 5 | + button { |
| 6 | + padding: 10px 20px; |
| 7 | + font-size: 16px; |
| 8 | + background-color: yellowgreen; |
| 9 | + color: darkred; |
| 10 | + border: thin; |
| 11 | + cursor: pointer; |
| 12 | + border-radius: 4px; |
| 13 | + } |
| 14 | + button:hover { |
| 15 | + background-color: #45a049; |
| 16 | + } |
| 17 | + </style> |
| 18 | +</head> |
| 19 | +<body> |
| 20 | + <h2>MultEQ-X measurement extractor by OCA</h2> |
| 21 | + <br> |
| 22 | + <label for="input">Select .mqx file to upload:</label> |
| 23 | + <input type="file" id="input" accept=".mqx"> |
| 24 | + <br> |
| 25 | + <p style="font-size: small; color: red;">Remember to upload ACM1HB standard mic calibration file (included in the ZIP) to REW before importing measurements for more accurate responses!</p> |
| 26 | + <br> |
| 27 | + <br> |
| 28 | + <button onclick="decodeAndConvert()"> |
| 29 | + Decode, convert and save<br>measurements for REW |
| 30 | + </button> |
| 31 | + <p id="message" style="display: none;">Please wait, working on the zip...</p> |
| 32 | + <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.0/FileSaver.min.js"></script> |
| 33 | + <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.6.0/jszip.min.js"></script> |
| 34 | + <script> |
| 35 | + async function decodeAndConvert() { |
| 36 | + const button = document.querySelector('button'); |
| 37 | + button.disabled = true; // Disable the button |
| 38 | + const message = document.getElementById('message'); |
| 39 | + message.style.display = 'block'; // Show the message |
| 40 | + const input = document.getElementById('input'); |
| 41 | + const file = input.files[0]; |
| 42 | + const text = await file.text(); |
| 43 | + const jsonData = JSON.parse(text); |
| 44 | + const channelMap = {}; |
| 45 | + const channelDataMap = jsonData._channelDataMap; |
| 46 | + for (const channelGuid in channelDataMap) { |
| 47 | + const metadata = channelDataMap[channelGuid].Metadata; |
| 48 | + channelMap[channelGuid] = metadata.AvrOriginatingDesignation; |
| 49 | + } |
| 50 | + const zip = new JSZip(); |
| 51 | + const measurements = jsonData._measurements; |
| 52 | + const trimPositionGuids = jsonData.CalibrationSettings.TrimPositionGuids; |
| 53 | + const distancePoisitionGuid = jsonData.CalibrationSettings.DistancePoisitionGuid; |
| 54 | + measurements.forEach((measurement, index) => { |
| 55 | + const base64String = measurement.Data; |
| 56 | + const bytesArray = new Uint8Array(atob(base64String).split('').map(c => c.charCodeAt(0))); |
| 57 | + const floats = []; |
| 58 | + for (let i = 0; i < bytesArray.length; i += 4) { |
| 59 | + floats.push(readFloat(bytesArray, i)); |
| 60 | + } |
| 61 | + const channelGuid = measurement.ChannelGuid; |
| 62 | + const positionGuid = measurement.PositionGuid; |
| 63 | + const avrOriginatingDesignation = channelMap[channelGuid]; |
| 64 | + const positionIndex = trimPositionGuids.indexOf(positionGuid); |
| 65 | +const isDistancePosition = positionGuid === distancePoisitionGuid; |
| 66 | +let filename; |
| 67 | + if (positionIndex === -1) { |
| 68 | + const shortenedGuid = positionGuid.substring(0, 3); |
| 69 | + filename = isDistancePosition |
| 70 | + ? `${avrOriginatingDesignation}_${shortenedGuid}_dp.txt` |
| 71 | + : `${avrOriginatingDesignation}_${shortenedGuid}.txt`; |
| 72 | + } else { |
| 73 | + filename = isDistancePosition |
| 74 | + ? `${avrOriginatingDesignation}_${positionIndex}_dp.txt` |
| 75 | + : `${avrOriginatingDesignation}_${positionIndex}.txt`; |
| 76 | + } |
| 77 | + const txtContent = `* Impulse Response data saved by REW |
| 78 | +0 // Peak value before normalisation |
| 79 | +0 // Peak index |
| 80 | +16384 // Response length |
| 81 | +2.0833333333333333E-5 // Sample interval (seconds) |
| 82 | +0.0 // Start time (seconds) |
| 83 | +* Data start |
| 84 | +${floats.join('\n')}`; |
| 85 | + zip.file(filename, txtContent); |
| 86 | + }); |
| 87 | + const zipBlob = await zip.generateAsync({ type: 'blob' }); |
| 88 | + saveAs(zipBlob, 'REW_Measurements.zip'); |
| 89 | + message.style.display = 'none'; // Hide the message |
| 90 | + button.disabled = false; // Enable the button |
| 91 | + } |
| 92 | + function readFloat(bytesArray, start) { |
| 93 | + const dv = new DataView(bytesArray.buffer); |
| 94 | + const intBits = dv.getUint32(start, true); |
| 95 | + const signBit = (intBits >> 31) & 0x1; |
| 96 | + const exponent = (intBits >> 23) & 0xFF; |
| 97 | + const mantissa = intBits & 0x7FFFFF; |
| 98 | + if (exponent === 0xFF) { |
| 99 | + if (mantissa === 0) { |
| 100 | + return signBit ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY; |
| 101 | + } else { |
| 102 | + return NaN; |
| 103 | + } |
| 104 | + } |
| 105 | + const bias = 127; |
| 106 | + const normalizedMantissa = 1 + mantissa / Math.pow(2, 23); |
| 107 | + const value = Math.pow(-1, signBit) * normalizedMantissa * Math.pow(2, exponent - bias); |
| 108 | + return value; |
| 109 | +} |
| 110 | + </script> |
| 111 | +</body> |
| 112 | +</html> |
0 commit comments