├── .github └── FUNDING.yml ├── .gitignore ├── FL_xx__x.odx.FD_2DATA.BIN.bt ├── LICENSE ├── README.md ├── firmwares ├── FL_1S0907159___0003_UNLOCKED.odx ├── aga-to-sak-gen2.5 │ └── FL_4M0907160A__0002_SAK_UNLOCKED.odx ├── aga-to-sak │ ├── FL_565907160A__0005_SAK_UNLOCKED.odx │ └── FL_8W0907160___0001_SAK_UNLOCKED.odx ├── diesel_570-6500rpm │ ├── FL_4G0907159B__0010_has_this_range_by_default.txt │ ├── FL_565907160A__0005_DIESEL_1_6500.odx │ ├── FL_5E0907159___0003_DIESEL_1_6500.odx │ ├── FL_5E0907159___0003_DIESEL_2_6500.odx │ ├── FL_8K0907159___0005_DIESEL_1_6500.odx │ ├── FL_8K0907159___0005_DIESEL_2_6500.odx │ ├── FL_8K0907159___0005_DIESEL_3_6500.odx │ ├── FL_8S0907159A__0005_DIESEL_1_6500.odx │ ├── FL_8S0907159___0004_DIESEL_1_6500.odx │ ├── FL_8S0907159___0004_DIESEL_2_6500.odx │ ├── FL_8S0907159___0004_DIESEL_3_6500.odx │ ├── FL_8W0907159D__0006_DIESEL_1_6500.odx │ ├── FL_8W0907159E__0007_DIESEL_1_6500.odx │ ├── FL_8W0907159E__0007_DIESEL_2_6500.odx │ ├── FL_8W0907159F__0005_DIESEL_1_6500.odx │ ├── FL_8W0907159F__0005_DIESEL_2_6500.odx │ ├── FL_8W0907159___0002_DIESEL_1_6500.odx │ ├── FL_8W0907159___0002_DIESEL_2_6500.odx │ └── FL_8W0907160___0001_DIESEL_1_6500.odx ├── diesel_600-5500rpm │ ├── FL_1S0907159___0003_DIESEL_1_5500.odx │ ├── FL_4G0907159B__0010_DIESEL_1_5500.odx │ ├── FL_4G0907160B__0007_has_this_range_by_default.txt │ ├── FL_565907160A__0005_DIESEL_1_5500.odx │ ├── FL_5E0907159___0003_DIESEL_1_5500.odx │ ├── FL_5E0907159___0003_DIESEL_2_5500.odx │ ├── FL_8K0907159___0005_DIESEL_1_5500.odx │ ├── FL_8K0907159___0005_DIESEL_2_5500.odx │ ├── FL_8K0907159___0005_DIESEL_3_5500.odx │ ├── FL_8S0907159A__0005_DIESEL_1_5500.odx │ ├── FL_8S0907159___0004_DIESEL_1_5500.odx │ ├── FL_8S0907159___0004_DIESEL_2_5500.odx │ ├── FL_8S0907159___0004_DIESEL_3_5500.odx │ ├── FL_8W0907159D__0006_DIESEL_1_5500.odx │ ├── FL_8W0907159E__0007_DIESEL_1_5500.odx │ ├── FL_8W0907159E__0007_DIESEL_2_5500.odx │ ├── FL_8W0907159F__0005_DIESEL_1_5500.odx │ ├── FL_8W0907159F__0005_DIESEL_2_5500.odx │ ├── FL_8W0907159___0002_DIESEL_1_5500.odx │ ├── FL_8W0907159___0002_DIESEL_2_5500.odx │ └── FL_8W0907160___0001_DIESEL_1_5500.odx ├── gen2.5-to-gen2 │ └── FL_4ML907159C__0005_GEN2_UNLOCKED.odx └── gen2.5 │ ├── FL_4K0907159E__0001.UNLOCKED.odx │ ├── FL_4K0907160A__0002.UNLOCKED.odx │ ├── FL_4K0907160B__0001.UNLOCKED.odx │ ├── FL_4M0907160A__0002.UNLOCKED.odx │ ├── FL_4M8907159A__0001.UNLOCKED.odx │ ├── FL_4M8907160A__0002.UNLOCKED.odx │ ├── FL_4ML907159C__0005.UNLOCKED.odx │ ├── FL_5FE907159___0001.UNLOCKED.odx │ ├── FL_5FF907159C__0001.UNLOCKED.odx │ ├── FL_5FF907159___0002.UNLOCKED.odx │ ├── FL_80A907160A__0002.UNLOCKED.odx │ ├── FL_8S0907159F__0002.UNLOCKED_1.odx │ ├── FL_8S0907159F__0002.UNLOCKED_2.odx │ ├── FL_8W0907159T__0001.UNLOCKED.odx │ ├── FL_8Y0907159A__0001.UNLOCKED.odx │ └── FL_9Y0907159M__0009.UNLOCKED.odx ├── images └── soundaktor.png └── tools ├── .gitignore ├── bin ├── bin2odx.exe └── odx2bin.exe ├── bin2odx.js ├── build.cmd ├── crc_calc.js ├── data2wav.js ├── odx2bin.js └── package.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: NumberOneBot 3 | custom: ['https://www.buymeacoffee.com/alexstrelets'] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.ignore/ -------------------------------------------------------------------------------- /FL_xx__x.odx.FD_2DATA.BIN.bt: -------------------------------------------------------------------------------- 1 | //------------------------------------------------ 2 | //--- 010 Editor v12.0 Binary Template 3 | // 4 | // File: FL_xx__x.odx.FD_2DATA.BIN 5 | // Authors: Jille, Dark 6 | // Version: 0.9.9 7 | // Purpose: Parse the contents of the MQB soundaktor firmware 8 | // Category: 9 | // File Mask: 10 | // ID Bytes: 11 | // History: 0.1 - Initial version 12 | // 0.2 - Additional field descriptions taken from MxCar sheet 13 | // 0.3.1 - 10 out of 12 tables mapped 14 | // 0.3.2 - Gain table key 15 | // 0.4 - Parsed and visualized all the tables found at this moment 16 | // 0.5 - Transposed some tables to be viewed in the right way 17 | // 0.6 - Added Lib_Sig (whatever it may be.. noise signature?) 18 | // 0.7 - template code cleanup, started GEN2.5 parsing 19 | // 0.8 - Checksum mechanism included 20 | // 0.9 - GEN2 finished, GEN2.5 work in progress 21 | // 0.9.1 - several more table params for GEN2.5 22 | // 0.9.8 - parsed and printed everything except SUM_DATA and MX_PROFILE 23 | // 0.9.9 - MX_Profile parsed 24 | // 1.0 - more GEN2.5 Car / MX Profile options parsed 25 | //------------------------------------------------ 26 | // legend: 27 | // yellow/aqua = uncertain 28 | // red = checksum 29 | // green = confirmed 30 | // gray = seemingly irrelevant 31 | // blue = special 32 | 33 | // LOCAL VARIABLES 34 | 35 | local int i = 0; 36 | local int j = 0; 37 | local float x = 0; 38 | local int factor = 0; 39 | local string format = ""; 40 | local int start = 0x02; 41 | local int end = 0xE000; 42 | local string output = ""; 43 | local int GEN25 = (FileSize() == 0x18000 ? 1 : 0); 44 | if (GEN25) { 45 | end = 0x18000; 46 | } 47 | 48 | string getFilterProperties(int filterSetting) { 49 | switch (filterSetting) { 50 | case 0x00: return "No";     51 | case 0x01: return "50Hz/6dB";       52 | case 0x02: return "100Hz/6dB";     53 | case 0x03: return "150Hz/6dB"; 54 | case 0x04: return "200Hz/6dB";     55 | case 0x05: return "250Hz/6dB";     56 | case 0x06: return "300Hz/6dB"; 57 | case 0x07: return "350Hz/6dB";     58 | case 0x08: return "400Hz/6dB";     59 | case 0x09: return "450Hz/6dB"; 60 | case 0x0A: return "50Hz/12dB";     61 | case 0x0B: return "100Hz/12dB";     62 | case 0x0C: return "150Hz/12dB"; 63 | case 0x0D: return "200Hz/12dB"; 64 | case 0x0E: return "250Hz/12dB";     65 | case 0x0F: return "300Hz/12dB"; 66 | case 0x10: return "350Hz/12dB"; 67 | case 0x11: return "400Hz/12dB";     68 | case 0x12: return "450Hz/12dB"; 69 | default: return "Unknown"; 70 | } 71 | } 72 | 73 | string getSampleRate(int sampleRate) { 74 | switch (sampleRate) { 75 | case 0: return "512"; 76 | case 1: return "1024"; 77 | case 2: return "2048"; 78 | case 3: return "4096"; 79 | case 4: return "8192"; 80 | case 5: return "16384"; 81 | case 6: return "32768"; 82 | default:return "Unknown"; 83 | } 84 | } 85 | 86 | int getIntegerSampleRate(int sampleRate) { 87 | switch (sampleRate) { 88 | case 0: return 512; 89 | case 1: return 1024; 90 | case 2: return 2048; 91 | case 3: return 4096; 92 | case 4: return 8192; 93 | case 5: return 16384; 94 | case 6: return 32768; 95 | default:return 0; 96 | } 97 | } 98 | 99 | string getSumEffect(int effectNumber) { 100 | switch(effectNumber) { 101 | case 0: return "Bypass"; 102 | case 1: return "Tiefpass"; 103 | case 2: return "Hochpass"; 104 | case 3: return "Bandpass"; 105 | case 4: return "Notch"; 106 | case 5: return "Peak"; 107 | case 6: return "LowShelf"; 108 | case 7: return "HighShelf"; 109 | default:return "Unknown"; 110 | } 111 | } 112 | 113 | 114 | // START PARSING 115 | 116 | LittleEndian(); 117 | 118 | ushort file_checksum ; //some kind of checksum? 119 | byte unknown_2 ; 120 | byte Gain_table_flag ; 121 | 122 | ushort DEFAULTS_Profiles ; 123 | if (!GEN25) { 124 | ushort DEFAULTS_Effects ; 125 | } else { 126 | local int DEFAULTS_Effects = 16; 127 | } 128 | ushort DEFAULTS_RevsFunctions ; // engine revolutions / LIB_n_Gain 129 | ushort DEFAULTS_AccelFunctions ; // acceleration / G-meter / LIB_m_Gain 130 | ushort DEFAULTS_SpeedFunctions ; // speed / LIB_v_Gain 131 | if (GEN25) { 132 | ushort DEFAULTS_PedalFunctions ; // pedal / LIB_p_Gain 133 | ushort DEFAULTS_Samples_count ; 134 | ushort unknown_9 ; 135 | ushort unknown_10 ; 136 | ushort unknown_11 ; 137 | ushort unknown_12 ; 138 | } else { 139 | ushort DEFAULTS_Revolutions_length ; 140 | ushort DEFAULTS_Acceleration_length ; 141 | ushort DEFAULTS_Speed_length ; 142 | ushort DEFAULTS_Acceleration_Table_size ; // size of LIB_m_Gain table in bytes, 512 143 | ushort DEFAULTS_Revolutions_Table_size ; // size of LIB_n_Gain table in bytes, 8192 144 | } 145 | 146 | ushort DEFAULTS_Profiles_2 ; 147 | if (!GEN25) { 148 | ushort DEFAULTS_Effects_2 ; 149 | } 150 | ushort DEFAULTS_RevsFunctions_2 ; 151 | ushort DEFAULTS_AccelFunctions_2 ; 152 | ushort DEFAULTS_SpeedFunctions_2 ; 153 | if (GEN25) { 154 | ushort DEFAULTS_PedalFunctions_2 ; 155 | ushort DEFAULTS_Samples_count_2 ; 156 | ushort DEFAULTS_Sample_Rate ; 157 | ushort unknown_14 ; 158 | ushort unknown_15 ; 159 | ushort unknown_16 ; 160 | ushort unknown_17 ; 161 | ushort unknown_18 ; 162 | } else { 163 | // the following 2 values are linked to SampleRate 164 | // Depending on the value there, the values here are different. 165 | ushort DEFAULTS_Samples_count ; // LIB_Sig 166 | ushort DEFAULTS_Sample_length ; // LIB_Sig 167 | ushort DEFAULTS_Damping_factor ; // Range 0-65535 168 | } 169 | 170 | // Offset where the filename starts. 171 | FSeek(62); 172 | ushort source_filename_checksum ;// CRC16-CCITT 173 | char source_filename[64] ; 174 | 175 | ushort Profiles ; 176 | ushort RevsFunctions ; 177 | ushort AccelFunctions ; 178 | ushort SpeedFunctions ; 179 | 180 | if (!GEN25) { 181 | ushort Samples_count ; 182 | ushort Sample_Rate ; 183 | ushort Damping_factor ; // 0 <= DF <= 1 Quantization 0-65535 184 | // they refer to it as initial speed value in GEN2 ???? (always set to 0!) 185 | ushort InitialSpeedValue ; // should be 0 in most cases, some electric cars have reversed value of 32768 186 | } else { 187 | 188 | ushort PedalFunctions ; 189 | ushort Samples_count ; 190 | ushort Sample_Rate ; 191 | ushort unknown_22 ; 192 | ushort unknown_23 ; 193 | ushort unknown_24 ; 194 | ushort unknown_25 ; 195 | ushort unknown_26 ; 196 | ushort unknown_27 ; 197 | ushort unknown_28 ; 198 | ushort unknown_29 ; 199 | ushort unknown_30 ; 200 | } 201 | 202 | Printf("%s \n\n", source_filename); 203 | if (!GEN25) { 204 | Printf("GEN2 firmware\n\n"); 205 | 206 | struct { 207 | ubyte Engine_code ; 208 | char VIN_mask[17] ; 209 | ubyte Charisma_01_Switch_Relapse_Clamp15 ; 210 | ubyte Charisma_02_Switch_Gearbox ; 211 | ubyte Charisma_03_Default_driving_profile ; 212 | ubyte Charisma_04_Reverse_driving_profile ; 213 | ubyte Charisma_05_Comfort_driving_profile ; 214 | ubyte Charisma_06_Normal_driving_profile ; 215 | ubyte Charisma_07_Sport_driving_profile ; 216 | ubyte Charisma_08_Offroad_driving_profile ; 217 | ubyte Charisma_09_Eco_driving_profile ; 218 | ubyte Charisma_10_Race_driving_profile ; 219 | ubyte Charisma_11_GTE_driving_profile ; 220 | ubyte HighPass_filter ; 221 | ubyte Reserved_for_future_use_2 ; 222 | ubyte PHEV_button_led_bits ; // 335 only 223 | } Car[Profiles]; 224 | 225 | // Engine codes 226 | 227 | // 06 - high-perf 2.0l diesel engine? CUNA / CUPA / CUBA 190hp+ 228 | // 07 - mid-perf DFGA (could cover DFE also, maybe CRL too) Kodiaq 150hp 2.0l diesel 229 | // 0C - CEPA / CZGB 2.5l I5 engine 230 | // 17 - DAZA 2.5l 340+hp 231 | // 29 - MLB-version of high-perf 2.0l 232 | // 2B - CJX 2.0l 220+hp GTI, S3, SQ2, etc 233 | // 2A - CJS 1.8 Leon FR 170+hp 234 | // 42 - CJE / CPK / CPR 1.8 Leon FR 235 | // 45 - something between CJX and higher-perf 2.5l engines, maybe overtuned CJX from Clubsport models? 260-280hp 236 | // 32 - Passat PHEV / Tiguan PHEV, 2.0l maybe? 237 | // 13 - CUK 1.4TSI GTE, Bora / Lavida / Golf 238 | // 14 - old-school A4 diesels 2.0l / 2.7l / 3.0l 239 | // 2D - A6 diesel ? 240 | // 48 - CWGD / DLZA V6 3.0l 350hp S4 241 | // 49 - DECA / DKMB V6 2.9l 450hp RS4 / RS5 242 | // 5A - ??? 243 | // 3F - ANY 244 | // D5 - S3 8Y 245 | // E4 - S3 8Y 246 | // D8 - Urus V8 247 | 248 | 249 | struct { // ST_Gain 250 | ushort value[DEFAULTS_Effects]; 251 | } Gain_Effect[Profiles] ; 252 | struct { // ST_Phase 253 | ushort value[DEFAULTS_Effects]; 254 | } Phase_Effect[Profiles] ; 255 | struct { // ST_Pitch_Shift 256 | ushort value[DEFAULTS_Effects]; 257 | } Pitch_Shift_Effect[Profiles] ; 258 | struct { // ST_Delay 259 | ushort value[DEFAULTS_Effects]; 260 | } Delay_Effect[Profiles] ; 261 | struct { 262 | byte value[DEFAULTS_Effects]; 263 | } Effects_to_Accel[Profiles] ; 264 | struct { 265 | byte value[DEFAULTS_Effects]; 266 | } Effects_to_Revs[Profiles] ; 267 | struct { 268 | byte value[DEFAULTS_Effects]; 269 | } Effects_to_Speed[Profiles] ; 270 | // general table to apply effects on the final sound of every car profile 271 | if (Gain_table_flag > 0) { 272 | // this gain table appears only in newer firmware: 5F 0003+, 8S, 8W 273 | // but 5F 0001, 0002, 8K firmwares dont have it 274 | struct { 275 | byte value[DEFAULTS_Effects]; 276 | } Effects_to_Profiles[Profiles] ; 277 | } 278 | struct { // MX_Sig 279 | ushort gain[Profiles]; 280 | ushort clip[Profiles]; 281 | } Effects_to_Samples ; 282 | ushort Acceleration_Scale[DEFAULTS_Acceleration_length] ; // values in [-0.3 - 0.98] range (5F 0003 fw) / [-0.4 - 1.00] range (565 AGA fw) 283 | ushort Revolutions_Scale[DEFAULTS_Revolutions_length] ; // values in [500 - 8150] (rpm) range quanified by 16256 284 | ushort Speed_Scale[DEFAULTS_Speed_length] ; // values in [0 - 326.39] (km/h) range 285 | struct { // LIB_m_Gain 286 | ushort value[DEFAULTS_Acceleration_length]; 287 | } Acceleration[AccelFunctions] ; 288 | struct { // LIB_n_Gain 289 | ushort value[DEFAULTS_Revolutions_length]; 290 | } Revolutions[RevsFunctions] ; 291 | struct { // LIB_v_Gain 292 | ushort value[DEFAULTS_Speed_length]; 293 | } Speed[SpeedFunctions] ; 294 | struct { // LIB_Sig 295 | short value[getIntegerSampleRate(Sample_Rate)]; 296 | } Samples[Samples_count] ; 297 | 298 | // Print a report to output 299 | Printf("ID\tEngine\tVIN\t\tRelapseKl15\tGearbox\tDefault\tReverse\tComfort(1)\tNormal(2)\tSport(3)\tOffroad(4)\tEco(5)\tRace(6)\tHybrid(8)\tHighPassFtr\tRFU2\tRFU3\t\n"); 300 | for( i = 0; i < Profiles; i++ ) { 301 | Printf("%i\t%02X\t%s\t%i\t%i\t%i\t%i\t%i\t%i\t%i\t%i\t%i\t%i\t%i\t%s\t%i\t%i\n", 302 | i+1, 303 | Car[i].Engine_code, 304 | Car[i].VIN_mask, 305 | Car[i].Charisma_01_Switch_Relapse_Clamp15, 306 | Car[i].Charisma_02_Switch_Gearbox, 307 | Car[i].Charisma_03_Default_driving_profile + 1,// these all need +1 for some reason, unless it's 255 308 | Car[i].Charisma_04_Reverse_driving_profile + 1, 309 | Car[i].Charisma_05_Comfort_driving_profile + 1, 310 | Car[i].Charisma_06_Normal_driving_profile + 1, 311 | Car[i].Charisma_07_Sport_driving_profile + 1, 312 | Car[i].Charisma_08_Offroad_driving_profile + 1, 313 | Car[i].Charisma_09_Eco_driving_profile + 1, 314 | Car[i].Charisma_10_Race_driving_profile + 1, 315 | Car[i].Charisma_11_GTE_driving_profile + 1, 316 | getFilterProperties(Car[i].HighPass_filter), 317 | Car[i].Reserved_for_future_use_2, 318 | Car[i].PHEV_button_led_bits 319 | ); 320 | } 321 | } else if (GEN25) { 322 | Printf("GEN2.5 firmware\n\n"); 323 | 324 | local int DEFAULTS_Acceleration_length = 32; 325 | local int DEFAULTS_Revolutions_length = 256; 326 | local int DEFAULTS_Speed_length = 32; 327 | local int DEFAULTS_Pedal_length = 32; 328 | 329 | FSeek(160); 330 | struct { 331 | ubyte Power_kW ; 332 | ubyte unknown_2 ; // car Class / Group 333 | ubyte Engine_code ; 334 | char VIN_mask[17] ; 335 | char Transmission ; 336 | // A - Auto 337 | // H - Hybrid ??? 338 | char BodyType ; 339 | // G - ??? 340 | // O - Cabrio / Open Roof 341 | char Drivetrain ; 342 | // V - All-wheel drive ??? 343 | // H - Hybrid 344 | char FPA_installed ; 345 | ubyte unknown_25 ; 346 | ubyte MX_ID ; // Priority ??? 347 | } Car[Profiles]; 348 | 349 | // local int MXP_y = 32; 350 | // local int MXP_x = 12; 351 | local int SUM_x = 14; // 19 352 | if (Gain_table_flag == 3) { 353 | SUM_x = 19; 354 | } 355 | 356 | // struct { 357 | // ubyte unknown[MXP_x]; 358 | // } 359 | ubyte MX_Profile[384] ; 360 | struct { 361 | ushort value[SUM_x]; // there is NO parameter with such a value and sometimes it is 19 !!!! 362 | } SUM_data[Profiles] ; 363 | struct { 364 | ushort value[DEFAULTS_Effects]; 365 | } Gain_Effect[Profiles] ; 366 | struct { 367 | ushort value[DEFAULTS_Effects]; 368 | } Pitch_Shift_Effect[Profiles] ; 369 | struct { 370 | ushort value[DEFAULTS_Effects]; 371 | } Phase_Effect[Profiles] ; 372 | struct { 373 | ushort value[DEFAULTS_Effects]; 374 | } Delay_Effect[Profiles] ; 375 | struct { 376 | ubyte value[DEFAULTS_Effects]; 377 | } Effects_to_Revs[Profiles] ; 378 | ushort Revolutions_Scale[DEFAULTS_Revolutions_length] ; // values in [500 - 8150] (rpm) range quanified by 16256 379 | struct { // LIB_n_Gain 380 | ushort value[DEFAULTS_Revolutions_length]; 381 | } Revolutions[RevsFunctions] ; 382 | struct { 383 | ubyte value[DEFAULTS_Effects]; 384 | } Effects_to_Accel[Profiles] ; 385 | ushort Acceleration_Scale[DEFAULTS_Acceleration_length] ; // values in [-0.5 - 1.00] range, acceleration 386 | struct { // LIB_m_Gain 387 | ushort value[DEFAULTS_Acceleration_length]; 388 | } Acceleration[AccelFunctions] ; 389 | struct { 390 | ubyte value[DEFAULTS_Effects]; 391 | } Effects_to_Speed[Profiles] ; 392 | ushort Speed_Scale[DEFAULTS_Speed_length] ; // values in [0 - 326.39] (km/h) range 393 | struct { // LIB_v_Gain 394 | ushort value[DEFAULTS_Speed_length]; 395 | } Speed[SpeedFunctions] ; 396 | struct { 397 | ubyte value[DEFAULTS_Effects]; 398 | } Effects_to_Pedal[Profiles] ; 399 | ushort Pedal_Scale[DEFAULTS_Pedal_length] ; 400 | struct { // LIB_p_Gain 401 | ushort value[DEFAULTS_Pedal_length]; 402 | } Pedal[PedalFunctions] ; 403 | struct { // MX_Sig 404 | ubyte value[DEFAULTS_Effects]; 405 | } Effects_to_Samples[Profiles] ; 406 | struct { // LIB_Sig 407 | short value[getIntegerSampleRate(Sample_Rate)]; 408 | } Samples[Samples_count] ; 409 | 410 | // Print a report to output 411 | Printf("CAR\n"); 412 | Printf("ID\tPower kW\tunknown_2\tEngine\tVIN\t\tTransmissn\tBody Type\tDrivetrain\tFPA Flag\tunknown_25\tMX ID\n"); 413 | for( i = 0; i < Profiles; i++ ) { 414 | Printf("%i\t%i\t%i\t%02X\t%s\t%s\t%s\t%s\t%s\t%i\t%i\n", 415 | i + 1, 416 | Car[i].Power_kW == 63 ? 0 : Car[i].Power_kW, 417 | Car[i].unknown_2, 418 | Car[i].Engine_code, 419 | Car[i].VIN_mask, 420 | Car[i].Transmission, 421 | Car[i].BodyType, 422 | Car[i].Drivetrain, 423 | (Car[i].FPA_installed == 'J' ? "Yes" : Car[i].FPA_installed == 'N' ? "No" : "Any"), 424 | Car[i].unknown_25, 425 | Car[i].MX_ID 426 | ); 427 | } 428 | } 429 | 430 | // FUNCTIONS 431 | 432 | void PrintHeader( char name[] ) { 433 | Printf("\n%s\n", name); 434 | if (!GEN25) { 435 | Printf("BOOST\t\t\tHYBRID\t\t\tEFFICIENCY\n"); 436 | Printf("Normal\tSport\tEco\tNormal\tSport\tEco\tNormal\tSport\n"); 437 | } 438 | } 439 | 440 | ubyte get2HalfByte(ubyte source) { 441 | return source >> 4; 442 | } 443 | 444 | ubyte get1HalfByte(ubyte source) { 445 | return source & 0x0F; 446 | } 447 | 448 | 449 | 450 | // SHORT spreadsheets 8x12(16) 451 | if (GEN25) { 452 | // WORK IN PROGRESS 453 | // WORK IN PROGRESS 454 | // WORK IN PROGRESS 455 | 456 | local int schema_type_byte_position = (Gain_table_flag == 3 ? 4 : 2); 457 | local int original_line_length = schema_type_byte_position + 4; 458 | local int current_line_length = original_line_length; 459 | local int line_counter = 0; 460 | local int profile = 0; 461 | j = 0; 462 | i = 0; 463 | 464 | Printf("\nMX_Profile\n"); 465 | Printf("ID\t byte_1\t"); 466 | if (schema_type_byte_position == 4) { 467 | Printf(" byte_2\t"); 468 | Printf(" byte_3\t"); 469 | } 470 | Printf("Schemas\t"); 471 | Printf(" Comfort(1)\t Normal(2)\t Sport(3)\t Offroad(4)\t Eco(5)\t Race(6)\t Hybrid(8)\t \t"); 472 | Printf(" Comfort(1)\t Normal(2)\t Sport(3)\t Offroad(4)\t Eco(5)\t Race(6)\t Hybrid(8)\t "); 473 | Printf("\n"); 474 | 475 | do { 476 | if (j == 0) { 477 | Printf("%i\t", line_counter+1); 478 | } 479 | j++; 480 | if (j == schema_type_byte_position && MX_Profile[i] > 0) { 481 | current_line_length +=4; 482 | } 483 | 484 | if ( j == schema_type_byte_position ) { 485 | profile = get1HalfByte(MX_Profile[i]); 486 | // if (profile > 127) { 487 | // profile = 255 - profile; 488 | // Printf("-"); 489 | // } 490 | 491 | if (profile == 0) { 492 | Printf("0 B\t"); 493 | } else if (profile == 1) { 494 | Printf("1 M\t"); 495 | } else if (profile == 2) { 496 | Printf("2 H\t"); 497 | } else if (profile == 3) { 498 | Printf("3 HM\t"); 499 | } else if (profile == 4) { 500 | Printf("4 C\t"); 501 | } else if (profile == 5) { 502 | Printf("5 CM\t"); 503 | } else if (profile == 6) { 504 | Printf("6 CH\t"); 505 | } else if (profile == 7) { 506 | Printf("7 CHM\t"); 507 | } else if (profile == 8) { 508 | Printf("8 BK\t"); 509 | } else { 510 | Printf("%i unkwn\t", profile); 511 | } 512 | } else { 513 | if ( j < schema_type_byte_position ) { 514 | Printf("( %02i %02i ) \t", get1HalfByte(MX_Profile[i]), get2HalfByte(MX_Profile[i]) ); 515 | } else { 516 | if ( j == schema_type_byte_position + 1 || 517 | j == schema_type_byte_position + 5 || 518 | j == schema_type_byte_position + 9 519 | ) { 520 | Printf("[ "); 521 | } 522 | if ( j == schema_type_byte_position + 4 || 523 | j == schema_type_byte_position + 8 || 524 | j == schema_type_byte_position + 12 525 | ) { 526 | Printf("%i\t]\t", get1HalfByte(MX_Profile[i]) + 1); // skip 8th unused half-byte 527 | } else { 528 | Printf("%i\t %i\t ", get1HalfByte(MX_Profile[i]) + 1, get2HalfByte(MX_Profile[i]) + 1); 529 | } 530 | 531 | } 532 | } 533 | 534 | if (j == current_line_length) { 535 | Printf("\n"); 536 | line_counter++; 537 | j = 0; 538 | current_line_length = original_line_length; 539 | } 540 | i++; 541 | } while (line_counter < 8); 542 | Printf("%i bytes length\n", i); 543 | 544 | /* 545 | // WORK IN PROGRESS 546 | // WORK IN PROGRESS 547 | // WORK IN PROGRESS 548 | Printf("\nSUM_data\n"); 549 | for ( j = 0; j < SUM_x; j++ ) { 550 | for( i = 0; i < Profiles; i++ ) { 551 | if (j == SUM_x-3) { 552 | x = (float) SUM_data[i].value[j] / 65536; 553 | Printf("%.2g\t", x); 554 | } else if (j == SUM_x-2) { 555 | x = (float) SUM_data[i].value[j] / 2048 * 16; 556 | Printf("%.2g\t", x); 557 | } else if (j == SUM_x-1) { 558 | x = (float) SUM_data[i].value[j] / 2048 * 8; 559 | Printf("%.2g\t", x); 560 | } else { 561 | x = (float) SUM_data[i].value[j] ; 562 | Printf("%g\t", x); 563 | } 564 | } 565 | Printf("\n"); 566 | }; 567 | // WORK IN PROGRESS 568 | // WORK IN PROGRESS 569 | // WORK IN PROGRESS 570 | */ 571 | } 572 | 573 | PrintHeader("Gain_Effect (ST/TS_Gain)"); 574 | for ( j = 0; j < DEFAULTS_Effects; j++ ) { 575 | for( i = 0; i < Profiles; i++ ) { 576 | if (GEN25) { 577 | x = (float) Gain_Effect[i].value[j] / (0xffff / 4); 578 | Printf("%.3g\t", x); 579 | } else { 580 | // there is definitelly a rounding issue exists 581 | // trying to compensate it with 1.0058 multiplier 582 | x = (float) Gain_Effect[i].value[j] * 1.0058 / 16; 583 | Printf("%.4g°\t", x); 584 | } 585 | } 586 | Printf("\n"); 587 | }; 588 | 589 | PrintHeader("Phase_Effect (ST/TS_Phase)"); 590 | for ( j = 0; j < DEFAULTS_Effects; j++ ) { 591 | for( i = 0; i < Profiles; i++ ) { 592 | x = (float) Phase_Effect[i].value[j]; 593 | if (GEN25) { 594 | x = (float) Phase_Effect[i].value[j] / (0xFFFF / (2 * 3.14159265359)); // 2Pi / 65536 595 | } 596 | Printf("%.3g\t", x); 597 | } 598 | Printf("\n"); 599 | }; 600 | 601 | PrintHeader("Pitch_Shift_Effect (ST/TS_Pitch-Shift)"); 602 | for ( j = 0; j < DEFAULTS_Effects; j++ ) { 603 | for( i = 0; i < Profiles; i++ ) { 604 | x = (float) Pitch_Shift_Effect[i].value[j] / 0xffff; 605 | if (GEN25) { 606 | x *= 1.4265; // FMax value 607 | } 608 | Printf("%.3g\t", x); 609 | } 610 | Printf("\n"); 611 | }; 612 | 613 | PrintHeader("Delay_Effect (ST/TS_Delay)"); 614 | for ( j = 0; j < DEFAULTS_Effects; j++ ) { 615 | for( i = 0; i < Profiles; i++ ) { 616 | x = (float) Delay_Effect[i].value[j] / 0xffff; 617 | if (GEN25) { 618 | x *= 0.01024; // 0.005 / 65536 619 | } 620 | Printf("%.2g\t", x); 621 | } 622 | Printf("\n"); 623 | }; 624 | 625 | // BYTE spreadsheets 8x12(16) 626 | 627 | PrintHeader("Effects_to_Accel (MX_m_Gain)"); 628 | for ( j = 0; j < DEFAULTS_Effects; j++ ) { 629 | for( i = 0; i < Profiles; i++ ) { 630 | Printf("%i\t", Effects_to_Accel[i].value[j] + 1); // not sure do we need this +1 631 | } 632 | Printf("\n"); 633 | }; 634 | 635 | PrintHeader("Effects_to_Revs (MX_n_Gain)"); 636 | for ( j = 0; j < DEFAULTS_Effects; j++ ) { 637 | for( i = 0; i < Profiles; i++ ) { 638 | Printf("%i\t", Effects_to_Revs[i].value[j] + 1); // not sure do we need this +1 639 | } 640 | Printf("\n"); 641 | }; 642 | 643 | PrintHeader("Effects_to_Speed (MX_v_Gain)"); 644 | for ( j = 0; j < DEFAULTS_Effects; j++ ) { 645 | for( i = 0; i < Profiles; i++ ) { 646 | Printf("%i\t", Effects_to_Speed[i].value[j] + 1); // not sure do we need this +1 647 | } 648 | Printf("\n"); 649 | }; 650 | 651 | if (GEN25) { 652 | PrintHeader("Effects_to_Pedal (MX_p_Gain)"); 653 | for ( j = 0; j < DEFAULTS_Effects; j++ ) { 654 | for( i = 0; i < Profiles; i++ ) { 655 | Printf("%i\t", Effects_to_Pedal[i].value[j] + 1); // not sure do we need this +1 656 | } 657 | Printf("\n"); 658 | }; 659 | 660 | PrintHeader("Effects_to_Samples (MX_Sig)"); 661 | for ( j = 0; j < DEFAULTS_Effects; j++ ) { 662 | for( i = 0; i < Profiles; i++ ) { 663 | Printf("%i\t", Effects_to_Samples[i].value[j] + 1); // not sure do we need this +1 664 | } 665 | Printf("\n"); 666 | }; 667 | } else { 668 | if (Gain_table_flag > 0) { 669 | PrintHeader("Effects_to_Profiles"); 670 | for ( j = 0; j < DEFAULTS_Effects; j++ ) { 671 | for( i = 0; i < Profiles; i++ ) { 672 | Printf("%i\t", Effects_to_Profiles[i].value[j] + 1); // not sure do we need this +1 673 | } 674 | Printf("\n"); 675 | }; 676 | } 677 | 678 | PrintHeader("Effects_to_Samples (MX_Sig)"); 679 | for ( j = 0; j < 8; j++ ) { 680 | Printf("%.3g\t", (float) Effects_to_Samples.gain[j] / 0xffff * 2); // 0 < X < 1 // Q:65536 681 | } 682 | Printf("GAIN"); 683 | Printf("\n"); 684 | for ( j = 0; j < 8; j++ ) { 685 | Printf("%.3g\t", (float) Effects_to_Samples.clip[j] / 0xffff * 512); // 0 < X < 8 // Q:65536 686 | } 687 | Printf("CLIP"); 688 | Printf("\n"); 689 | } 690 | 691 | Printf("\nAcceleration (LIB_m_Gain)\nHEADER\tDATA\n"); 692 | for( i = 0; i < DEFAULTS_Acceleration_length; i++ ) { 693 | Printf("%.2f", (float) Acceleration_Scale[i] / 32768 - 1); 694 | for( j = 0; j < AccelFunctions; j++ ) { 695 | Printf("\t%.4f", (float) Acceleration[j].value[i] / 0xffff); 696 | } 697 | Printf("\n"); 698 | } 699 | 700 | Printf("\nRevolutions (LIB_n_Gain)\nHEADER\tDATA\n"); 701 | for( i = 0; i < DEFAULTS_Revolutions_length; i++ ) { 702 | Printf("%i", (float) Revolutions_Scale[i] / 4); 703 | for( j = 0; j < 16; j++ ) { 704 | Printf("\t%.4f", (float) Revolutions[j].value[i] / 0xffff); 705 | } 706 | Printf("\n"); 707 | } 708 | 709 | Printf("\nSpeed (LIB_v_Gain)\nHEADER\tDATA\n"); 710 | for( i = 0; i < DEFAULTS_Speed_length; i++ ) { 711 | // real scale for this header column is the percentage [0 - 100%] from 326.39 value 712 | // multipliying it to 3.2639 shows absolute speed 713 | // (VW e-UP! is limited to 150kmh) 714 | Printf("%.2f", (float) Speed_Scale[i] / 326.39 * 3.2639); 715 | for( j = 0; j < SpeedFunctions; j++ ) { 716 | Printf("\t%.4f", (float) Speed[j].value[i] / 0xffff); 717 | } 718 | Printf("\n"); 719 | } 720 | 721 | if (GEN25) { 722 | Printf("\nPedal (LIB_p_Gain)\nHEADER\tDATA\n"); 723 | for( i = 0; i < DEFAULTS_Pedal_length; i++ ) { 724 | Printf("%.2f", (float) Pedal_Scale[i] / 0xffff); 725 | for( j = 0; j < PedalFunctions; j++ ) { 726 | Printf("\t%.4f", (float) Pedal[j].value[i] / 0xffff); 727 | } 728 | Printf("\n"); 729 | } 730 | } 731 | /* 732 | Printf("\nSamples (LIB_Sig) %i length x %i\nHEADER\tDATA\n", getIntegerSampleRate(Sample_Rate), Samples_count); 733 | for( i = 0; i < getIntegerSampleRate(Sample_Rate); i++ ) { 734 | for( j = 0; j < Samples_count; j++ ) { 735 | Printf("%i\t", (float) Samples[j].value[i]); 736 | } 737 | Printf("\n"); 738 | } 739 | */ 740 | 741 | local int checksum = -1; 742 | for (j = start;j < end; j += 2) { 743 | checksum = checksum + ReadUShort(j); 744 | } 745 | checksum = (checksum & 0xffff) ^ 0xffff; 746 | local int name_checksum = Checksum(CHECKSUM_CRCCCITT, 64, 64, -1, -1); // algo, size, length, poly, init 747 | Printf("\nFILE CHECKSUM: %.04X", SwapBytes(checksum) >> 16 & 0x0000ffff); 748 | Printf("\nNAME CHECKSUM: %.04X", SwapBytes(name_checksum) >> 16 & 0x0000ffff); 749 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alex Strelets 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MQB soundaktor 2 | 3 | The soundaktor (German for "sound actuator") is a vehicle audio system used to simulate engine noise in the cabin of MQB / MLB / MLB-evo vehicles. 4 | 5 | ![Soundaktor](https://github.com/jilleb/mqb-soundaktor/blob/9e37063de8729258620701f0f63c91376dcf7cd0/images/soundaktor.png) 6 | 7 | This repository is meant to research the details inside the soundaktor firmware, in specific the FL_xx__x.odx.FD_2DATA.BIN file. 8 | 9 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://www.buymeacoffee.com/alexstrelets) 10 | 11 | ## Requirements 12 | 13 | - A MQB platform vehicle 14 | - MQB soundaktor 15 | - Hex-editing skills 16 | - [010 editor](https://www.sweetscape.com/010editor/) 17 | - A good pair of brains 18 | - Common sense 19 | - No fear of bricking your hardware or losing your warranty 20 | - An inquisitive mindset 21 | - No bad intentions. 22 | 23 | ## Usage 24 | 25 | ### FL_xx__x.odx.FD_2DATA.BIN.bt 26 | 27 | This is the main template, which can parse raw binary data from a dataset in 010 editor. 28 | 29 | ## Wiki 30 | 31 | The place where the entire dataset is described and explored. Check it out [here](../..//wiki). 32 | 33 | ## Contributing 34 | 35 | Everyone can contribute to this project in some way. You can make your own dataset changes and test them, and report back. If you have any suggestions or chagnes to the template or script, feel free to make a pull request. 36 | 37 | ## Disclaimer 38 | 39 | Most of the knowledge shared on this repository was gathered, analyzed and tested by me and friends. Be careful when you edit "random stuff" inside the firmware files. Most of it is pretty harmless, but if there's a warning somewhere you can definitely count on it that this warning came from experiencing some crazy situation. So don't be a douche, be careful. And don't be a super-douche: don't sell this knowledge as your own property. In short: Be careful, be safe, be nice. Thanks! 40 | 41 | Also: this research is strictly for fun and for the educational value. It's never my intention to harm anyone, any car or any company. As far as my limited legal knowledge goes, there's nothing illegal to this research. If you think it might break any law or can be harmful in any way to anyone, please let me know. 42 | 43 | 44 | 45 | ## License 46 | 47 | See [LICENSE](https://github.com/NumberOneBot/mqb-soundaktor/blob/main/LICENSE) for more information. 48 | -------------------------------------------------------------------------------- /firmwares/diesel_570-6500rpm/FL_4G0907159B__0010_has_this_range_by_default.txt: -------------------------------------------------------------------------------- 1 | ! -------------------------------------------------------------------------------- /firmwares/diesel_600-5500rpm/FL_4G0907160B__0007_has_this_range_by_default.txt: -------------------------------------------------------------------------------- 1 | ! -------------------------------------------------------------------------------- /images/soundaktor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NumberOneBot/mqb-soundaktor/1eb1a922ca1bafd470786f7c871249501eb9d87d/images/soundaktor.png -------------------------------------------------------------------------------- /tools/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /test_data/ 3 | /package-lock.json -------------------------------------------------------------------------------- /tools/bin/bin2odx.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NumberOneBot/mqb-soundaktor/1eb1a922ca1bafd470786f7c871249501eb9d87d/tools/bin/bin2odx.exe -------------------------------------------------------------------------------- /tools/bin/odx2bin.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NumberOneBot/mqb-soundaktor/1eb1a922ca1bafd470786f7c871249501eb9d87d/tools/bin/odx2bin.exe -------------------------------------------------------------------------------- /tools/bin2odx.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const polycrc = require('polycrc'); 3 | const clc = require('cli-color'); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | 6 | if (!argv._.length) { 7 | console.log(clc.red('No file name provided')); 8 | return; 9 | } 10 | 11 | const { 12 | _: [filename, newFilename = filename.replace('.odx', '.MODIFIED.odx')], 13 | format = 'bin', 14 | debug = false, 15 | } = argv; 16 | 17 | const byteSwap = (str) => str.slice(2, 4) + str.slice(0, 2); 18 | const prettify = (value, length = 4) => value.toString(16).toUpperCase().padStart(length, '0'); 19 | const DATA02length = 0xe000; 20 | const DATA02length25 = 0x18000; 21 | const crc32Calculator = polycrc.crc(32, 0x04c11db7, 0, 0, true); // size, poly, init, xorout, refin/refout 22 | const crc16Calculator = polycrc.crc(16, 0x1021, 0xffff, 0, false); // size, poly, init, xorout, refin/refout 23 | 24 | fs.readFile(filename, 'utf8', (err, data) => { 25 | if (err) return console.log(err); 26 | 27 | const names = [...data.matchAll(/ val[1]), 28 | hexData = [...data.matchAll(/(\S+)<\/DATA>/gims)].map((val) => val[1]), 29 | crc32Data = {}; 30 | 31 | [ 32 | ...data.matchAll( 33 | /(\S+?)<\/FW-CHECKSUM>.*?(\S+?)<\/VALIDITY-FOR>/gims, 34 | ), 35 | ].map((val) => (crc32Data[val[2]] = { value: val[1], match: val[0] })); 36 | 37 | let writeFlag = false; 38 | names 39 | .filter((val) => val.indexOf('ERASE') === -1) 40 | .map((val) => { 41 | try { 42 | const originalData = hexData[names.indexOf(val)]; 43 | let rawData = fs.readFileSync(`${filename}.${val}.${format}`); 44 | if (format !== 'bin') { 45 | rawData = Uint8Array.from( 46 | String(rawData) 47 | .match(/(..)/g) 48 | .map((val) => parseInt(val, 16)), 49 | ); 50 | } 51 | let newData = [...rawData].map((char) => prettify(char, 2)).join(''); 52 | 53 | if (originalData === newData) { 54 | console.log(clc.green((val + '.' + format).padEnd(14)) + ` didn't change`); 55 | } else { 56 | writeFlag = true; 57 | console.log(clc.yellow((val + '.' + format).padEnd(14)) + ` has been modified`); 58 | 59 | if (rawData.length === DATA02length || rawData.length === DATA02length25) { 60 | const xlsBinName = rawData.slice(64, 128), 61 | xlsCrc = byteSwap(prettify(crc16Calculator(xlsBinName), 4)); 62 | // inject new checksum into binary array 63 | rawData[62] = parseInt(xlsCrc.slice(0, 2), 16); 64 | rawData[63] = parseInt(xlsCrc.slice(2, 4), 16); 65 | // construct new hex data for ODX container 66 | newData = 67 | newData.slice(0, 124) + xlsCrc + newData.slice(128, rawData.length * 2); 68 | // calc file checksum 69 | let fileCrc = -1; 70 | for (let i = 2; i < rawData.length; i += 2) { 71 | // convert two separate bytes into little endian short 72 | const ushort = rawData[i] + rawData[i + 1] * 256; 73 | fileCrc += ushort; 74 | } 75 | fileCrc = (fileCrc & 0xffff) ^ 0xffff; 76 | fileCrc = byteSwap(prettify(fileCrc, 4)); 77 | // inject new checksum into binary array 78 | rawData[0] = parseInt(fileCrc.slice(0, 2), 16); 79 | rawData[1] = parseInt(fileCrc.slice(2, 4), 16); 80 | // construct new hex data for ODX container 81 | newData = fileCrc + newData.slice(4, rawData.length * 2); 82 | 83 | if (debug) { 84 | if (rawData.length === DATA02length) { 85 | console.log(clc.magenta(`GEN2 firmware`)); 86 | } else { 87 | console.log(clc.magenta(`GEN2.5 firmware`)); 88 | } 89 | console.log(clc.magenta(`DATA checksum ${fileCrc}`)); 90 | console.log(clc.magenta(`NAME checksum ${xlsCrc}`)); 91 | } 92 | } 93 | 94 | const { value: originalCrc, match: originalCrcMatch } = 95 | crc32Data[val.replace('FD_', 'DB_')], 96 | odxCrc = prettify(crc32Calculator(rawData), 8); 97 | data = data.replace(originalData, newData); 98 | data = data.replace( 99 | originalCrcMatch, 100 | originalCrcMatch.replace(originalCrc, odxCrc), 101 | ); 102 | 103 | if (debug) { 104 | console.log(clc.magenta(`ODX checksum ${odxCrc}`)); 105 | } 106 | } 107 | } catch (e) { 108 | console.log(clc.red(`error reading block ${val}.${format}`)); 109 | console.log(e); 110 | } 111 | }); 112 | 113 | if (writeFlag) { 114 | fs.writeFile(newFilename, data, (err) => { 115 | if (err) return console.log(err); 116 | console.log(clc.cyan(newFilename) + ` created`); 117 | }); 118 | } 119 | }); 120 | -------------------------------------------------------------------------------- /tools/build.cmd: -------------------------------------------------------------------------------- 1 | npx pkg odx2bin.js -t node16-win-x64 -o bin/odx2bin.exe -C GZip & npx pkg bin2odx.js -o bin/bin2odx.exe -t node16-win-x64 -C GZip -------------------------------------------------------------------------------- /tools/crc_calc.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const polycrc = require('polycrc'); 3 | const Hashes = require('jshashes'); 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | 6 | if (!argv._.length) { 7 | console.log('No file name provided'); 8 | return; 9 | } 10 | 11 | const { _: filenames } = argv; 12 | let content = [], 13 | crcs = [], 14 | combinedContent = ''; 15 | 16 | const arr2string = (arr) => arr.map((char) => String.fromCharCode(char)).join(''); 17 | const crc2string = (crc) => crc.toString(16).toUpperCase().padStart(4, '0'); 18 | const byte_switch = (str) => str.slice(2, 4) + str.slice(0, 2); 19 | //const crc16 = polycrc.crc(16, 0x1021, 0, 0, true); 20 | const crc16 = polycrc.crc(16, 0x1021, 0xffff, 0, false); 21 | 22 | const reverseBits = (num) => { 23 | let reversed = num.toString(2); 24 | const padding = '0'; 25 | reversed = padding.repeat(16 - reversed.length) + reversed; 26 | return parseInt(reversed.split('').reverse().join(''), 2); 27 | }; 28 | const MD5 = new Hashes.MD5(), 29 | SHA1 = new Hashes.SHA1(), 30 | SHA256 = new Hashes.SHA256(), 31 | SHA512 = new Hashes.SHA512(), 32 | RMD160 = new Hashes.RMD160(); 33 | 34 | String.prototype.splice = function (idx, rem, str) { 35 | return this.slice(0, idx) + str + this.slice(idx + Math.abs(rem)); 36 | }; 37 | 38 | // 8S__0004 : 47 8E 39 | // 3G__0006 : 26 5B 40 | // 5G__0014 : 0F 93 41 | 42 | filenames.map((filename, index) => { 43 | try { 44 | let data = fs.readFileSync(filename); 45 | let dataArray = [...data]; 46 | 47 | if (index === 0) { 48 | const xlsFilename = arr2string(dataArray.slice(64, 128)); 49 | const xlsCrc = crc2string(crc16(xlsFilename)), 50 | xlsCrcByteSwitch = byte_switch(xlsCrc); 51 | console.log( 52 | `XLS:\n${xlsFilename}\nCRC16: \x1b[32m${xlsCrc}\x1b[0m byte_switch \x1b[31m${xlsCrcByteSwitch} \x1b[0m` 53 | ); 54 | 55 | dataArray = dataArray.slice(2); 56 | // dataArray = dataArray.slice(4); 57 | // dataArray = [0, 0, ...dataArray.slice(2)]; 58 | // dataArray = [0xff, 0xff, ...dataArray.slice(2)]; 59 | } 60 | let dataString = arr2string(dataArray); 61 | content.push(dataString); 62 | crcs.push(crc16(dataString)); 63 | 64 | console.log( 65 | `\n${filename}\ndata length: %i\nCRC16: \x1b[32m%s\x1b[0m xor \x1b[33m%s\x1b[0m rev \x1b[34m%s\x1b[0m r+x \x1b[35m%s\x1b[0m`, 66 | dataArray.length, 67 | crc2string(crc16(dataString)), 68 | crc2string(crc16(dataString) ^ 0xffff), 69 | crc2string(reverseBits(crc16(dataString))), 70 | crc2string(reverseBits(crc16(dataString) ^ 0xffff)) 71 | ); 72 | const md5 = MD5.hex(dataString), 73 | sha1 = SHA1.hex(dataString), 74 | sha256 = SHA256.hex(dataString), 75 | rmd160 = RMD160.hex(dataString); 76 | 77 | console.log( 78 | `MD5: %s\x1b[0m\t\t\t\t \x1b[33m%s\x1b[0m \x1b[34m%s\x1b[0m \x1b[35m%s\x1b[0m`, 79 | md5.toUpperCase().splice(-4, 0, '\x1b[32m'), 80 | crc2string(parseInt(md5.slice(-4), 16) ^ 0xffff), 81 | crc2string(reverseBits(parseInt(md5.slice(-4), 16))), 82 | crc2string(reverseBits(parseInt(md5.slice(-4), 16) ^ 0xffff)) 83 | ); 84 | console.log( 85 | `SHA1: %s\x1b[0m\t\t\t \x1b[33m%s\x1b[0m \x1b[34m%s\x1b[0m \x1b[35m%s\x1b[0m`, 86 | sha1.toUpperCase().splice(-4, 0, '\x1b[32m'), 87 | crc2string(parseInt(sha1.slice(-4), 16) ^ 0xffff), 88 | crc2string(reverseBits(parseInt(sha1.slice(-4), 16))), 89 | crc2string(reverseBits(parseInt(sha1.slice(-4), 16) ^ 0xffff)) 90 | ); 91 | console.log( 92 | `SHA256: %s\x1b[0m \x1b[33m%s\x1b[0m \x1b[34m%s\x1b[0m \x1b[35m%s\x1b[0m`, 93 | sha256.toUpperCase().splice(-4, 0, '\x1b[32m'), 94 | crc2string(parseInt(sha256.slice(-4), 16) ^ 0xffff), 95 | crc2string(reverseBits(parseInt(sha256.slice(-4), 16))), 96 | crc2string(reverseBits(parseInt(sha256.slice(-4), 16) ^ 0xffff)) 97 | ); 98 | console.log( 99 | `RMD160: %s\x1b[0m\t\t\t \x1b[33m%s\x1b[0m \x1b[34m%s\x1b[0m \x1b[35m%s\x1b[0m`, 100 | rmd160.toUpperCase().splice(-4, 0, '\x1b[32m'), 101 | crc2string(parseInt(rmd160.slice(-4), 16) ^ 0xffff), 102 | crc2string(reverseBits(parseInt(rmd160.slice(-4), 16))), 103 | crc2string(reverseBits(parseInt(rmd160.slice(-4), 16) ^ 0xffff)) 104 | ); 105 | } catch (err) { 106 | console.log(err); 107 | } 108 | }); 109 | const combined = content.join(''); 110 | console.log( 111 | `\nCOMBINED\ndata length: %i\nCRC16: \x1b[32m%s\x1b[0m xor \x1b[33m%s\x1b[0m rev \x1b[34m%s\x1b[0m r+x \x1b[35m%s\x1b[0m`, 112 | combined.length, 113 | crc2string(crc16(combined)), 114 | crc2string(crc16(combined) ^ 0xffff), 115 | crc2string(reverseBits(crc16(combined))), 116 | crc2string(reverseBits(crc16(combined) ^ 0xffff)) 117 | ); 118 | 119 | const md5 = MD5.hex(combined), 120 | sha1 = SHA1.hex(combined), 121 | sha256 = SHA256.hex(combined), 122 | rmd160 = RMD160.hex(combined); 123 | 124 | console.log( 125 | `MD5: %s\x1b[0m\t\t\t\t \x1b[33m%s\x1b[0m \x1b[34m%s\x1b[0m \x1b[35m%s\x1b[0m`, 126 | md5.toUpperCase().splice(-4, 0, '\x1b[32m'), 127 | crc2string(parseInt(md5.slice(-4), 16) ^ 0xffff), 128 | crc2string(reverseBits(parseInt(md5.slice(-4), 16))), 129 | crc2string(reverseBits(parseInt(md5.slice(-4), 16) ^ 0xffff)) 130 | ); 131 | console.log( 132 | `SHA1: %s\x1b[0m\t\t\t \x1b[33m%s\x1b[0m \x1b[34m%s\x1b[0m \x1b[35m%s\x1b[0m`, 133 | sha1.toUpperCase().splice(-4, 0, '\x1b[32m'), 134 | crc2string(parseInt(sha1.slice(-4), 16) ^ 0xffff), 135 | crc2string(reverseBits(parseInt(sha1.slice(-4), 16))), 136 | crc2string(reverseBits(parseInt(sha1.slice(-4), 16) ^ 0xffff)) 137 | ); 138 | console.log( 139 | `SHA256: %s\x1b[0m \x1b[33m%s\x1b[0m \x1b[34m%s\x1b[0m \x1b[35m%s\x1b[0m`, 140 | sha256.toUpperCase().splice(-4, 0, '\x1b[32m'), 141 | crc2string(parseInt(sha256.slice(-4), 16) ^ 0xffff), 142 | crc2string(reverseBits(parseInt(sha256.slice(-4), 16))), 143 | crc2string(reverseBits(parseInt(sha256.slice(-4), 16) ^ 0xffff)) 144 | ); 145 | // console.log(`SHA512: %s\x1b[0m `, SHA512.hex(combined).splice(-4, 0, '\x1b[32m')); 146 | console.log( 147 | `RMD160: %s\x1b[0m\t\t\t \x1b[33m%s\x1b[0m \x1b[34m%s\x1b[0m \x1b[35m%s\x1b[0m`, 148 | rmd160.toUpperCase().splice(-4, 0, '\x1b[32m'), 149 | crc2string(parseInt(rmd160.slice(-4), 16) ^ 0xffff), 150 | crc2string(reverseBits(parseInt(rmd160.slice(-4), 16))), 151 | crc2string(reverseBits(parseInt(rmd160.slice(-4), 16) ^ 0xffff)) 152 | ); 153 | -------------------------------------------------------------------------------- /tools/data2wav.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const wav = require('node-wav'); 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv._.length) { 6 | console.log('No file name provided'); 7 | return; 8 | } 9 | 10 | // Parameters for the data below 11 | const { 12 | _: [filename, wavFilename = filename.replace(/\.[^/.]+$/, '.wav')], 13 | rate: sampleRate = 2048, 14 | factor: sampleFactor = 1, 15 | repeat: sampleRepeat = 2, 16 | columns = 16, 17 | stereo = false, 18 | delay: channelDelay = 0, 19 | } = argv; 20 | 21 | fs.readFile(filename, 'utf8', (err, data) => { 22 | if (err) return console.log(err); 23 | 24 | const table = data.split('\n').map((line) => line.split('\t')); 25 | const samples = [...Array(columns).keys()] 26 | .map((i) => 27 | Array(sampleRepeat) 28 | .fill(table.map((vals) => parseInt(vals[i] / sampleFactor, 10))) 29 | .flat() 30 | ) 31 | .flat(); 32 | 33 | // mono 34 | let channels = [samples]; 35 | // add second channel with some delay 36 | if (stereo) { 37 | channels.push([...samples.splice(1024 - channelDelay, channelDelay), ...samples]); 38 | } 39 | // console.log(channels); 40 | let buffer = wav.encode(channels, { 41 | sampleRate, 42 | float: false, 43 | bitDepth: 16, 44 | }); 45 | fs.writeFile(wavFilename, buffer, (err) => { 46 | if (err) return console.log(err); 47 | 48 | console.log(`\x1b[36m${wavFilename} created\x1b[0m`); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tools/odx2bin.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const clc = require('cli-color'); 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | if (!argv._.length) { 6 | console.log('No file name provided'); 7 | return; 8 | } 9 | 10 | const { 11 | _: [filename], 12 | format = 'bin', 13 | } = argv; 14 | 15 | fs.readFile(filename, 'utf8', (err, data) => { 16 | if (err) return console.log(err); 17 | 18 | const names = [...data.matchAll(/ val[1]), 19 | hexData = [...data.matchAll(/(\S+)<\/DATA>/gims)].map((val) => val[1]); 20 | 21 | names.map((val, i) => { 22 | if (val.indexOf('ERASE') === -1) { 23 | const blockFilename = `${filename}.${val}.${format}`; 24 | fs.writeFile( 25 | blockFilename, 26 | format === 'bin' 27 | ? Uint8Array.from(hexData[i].match(/(..)/g).map((val) => parseInt(val, 16))) 28 | : hexData[i], 29 | (err) => { 30 | if (err) return console.log(err); 31 | console.log(clc.cyan(`${blockFilename}`) + `\tcreated`); 32 | } 33 | ); 34 | } 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "pkg": "^5.5.2", 4 | "jshashes": "^1.0.8", 5 | "minimist": "^1.2.5", 6 | "node-wav": "^0.0.2", 7 | "polycrc": "^1.1.0", 8 | "cli-color": "^2.0.1" 9 | }, 10 | "devDependencies": { 11 | "eslint": "^8.6.0" 12 | } 13 | } 14 | --------------------------------------------------------------------------------