From 0476035c94b59e46388b73bb89d07068bebf3b84 Mon Sep 17 00:00:00 2001 From: markusschloesser <59286549+markusschloesser@users.noreply.github.com> Date: Tue, 25 Feb 2025 01:17:37 +0100 Subject: [PATCH 01/13] Bank management and sending from kk to synth currently NOT working (cherry picked from commit fa51671243cb2056c55ce1cf087830e7636d202e) --- adaptations/KawaiK5000.py | 413 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 adaptations/KawaiK5000.py diff --git a/adaptations/KawaiK5000.py b/adaptations/KawaiK5000.py new file mode 100644 index 00000000..8cde815b --- /dev/null +++ b/adaptations/KawaiK5000.py @@ -0,0 +1,413 @@ +# W, S and R have Bank A +# S and R have Bank D +# If expansion installed also Banks E and F +# W additionally for the Rom parts Bank B + +# https://github.com/coniferprod/KSynthLib/blob/master/KSynthLib/K5000/SystemExclusive.cs + +import struct +from typing import List, Dict, Any +from knobkraft.sysex import findSysexDelimiters + +K5000_SPECIFIC_DEVICE = None + +KawaiSysexID = 0x40 +OneBlockDumpRequest = 0x00 # get one patch +AllBlockDumpRequest = 0x01 +OneBlockDump = 0x20 +AllBlockDump = 0x21 + +# Handshake stuff +WriteComplete = 0x40, +WriteError = 0x41, +WriteErrorByProtect = 0x42, +WriteErrorByMemoryFull = 0x44, +WriteErrorByNoExpandMemory = 0x45 + + +def name(): + return "Kawai K5000" + + +def createDeviceDetectMessage(channel): # ✅ + return [0xF0, KawaiSysexID, channel, 0x60, 0xF7] + + +def channelIfValidDeviceResponse(message): # ✅ + global K5000_SPECIFIC_DEVICE + # Check minimum length to avoid out-of-bounds errors + if len(message) != 8: + return -1 + + # Verify Sysex header for Kawai K5000 + if (message[0] == 0xF0 # Start of SysEx + and message[1] == KawaiSysexID # Kawai manufacturer ID + and 0x00 <= message[2] <= 0x0F # Unit channel (0-F for ch 1-16) + and message[3] == 0x61 # Fixed ID for this SysEx type + and message[4] == 0x00 # Reserved, always 00 + and message[5] == 0x0A # Specific function ID for ID request reply + and message[7] == 0xF7): # End of SysEx + + # Extract channel number (0-F, corresponding to 1-16 MIDI channels) + channel = message[2] + + # Device ID mapping + device_map = { + 0x01: "K5000W", + 0x02: "K5000S", + 0x03: "K5000R" + } + + # Extract device type + K5000_SPECIFIC_DEVICE = device_map.get(message[6], "Unknown Device") + + return channel + + return -1 + + +def needsChannelSpecificDetection(): # ✅ + return True + + +def bankDescriptors() -> List[Dict]: + global K5000_SPECIFIC_DEVICE + + if K5000_SPECIFIC_DEVICE is None: + return [] # Prevent errors if called before detection + + base_banks = [(0x00, "A", 100)] # "100" needs to be changed back to 128 once kk can do skipping + + if K5000_SPECIFIC_DEVICE == "K5000W": + base_banks.append((0x01, "B", 128)) # ROMpler Bank for W + + if K5000_SPECIFIC_DEVICE in ["K5000S", "K5000R"]: + base_banks.append((0x02, "D", 40)) # needs to be changed back to 128 once kk can do skipping + base_banks.extend([(0x03, "E", 51), (0x04, "F", 128)]) # Expansion banks # needs to be changed back to 128 once kk can do skipping + + return [{ + "bank": b[0], + "name": f"Bank {b[1]}", + "size": b[2], + "type": "Patch", + "isROM": (b[1] == "B") # Only ROM for K5000W Bank B + } for b in base_banks] + + +def createEditBufferRequest(channel): + # Not implemented - the Kawai K5000 can not be requested to send its edit buffer + return [] + + +def createProgramDumpRequest(channel, patchNo): # ✅ + global K5000_SPECIFIC_DEVICE + + banks = bankDescriptors() + total_patches = 0 + selected_bank = None + patch_number = patchNo + + # Correct mapping of bank indexes to SysEx bank bytes + bank_byte_map = { + "A": 0x00, + "D": 0x02, + "E": 0x03, + "F": 0x04 + } + + for bank in banks: + if patch_number < bank["size"]: + selected_bank = bank + break + patch_number -= bank["size"] + + if selected_bank is None: + raise ValueError(f"Invalid patch number {patchNo}. Exceeds total patch count.") + + bank_name = selected_bank["name"].split()[-1] # Extract the letter (A, D, E, F) + bank_byte = bank_byte_map.get(bank_name, 0x00) # Default to A if something goes wrong + + sysex_message = [ + 0xF0, KawaiSysexID, channel, OneBlockDumpRequest, 0x00, 0x0A, 0x00, bank_byte, patch_number, 0xF7 + ] + return sysex_message + + +def isSingleProgramDump(message): # ✅ + # Check minimum length to avoid out-of-bounds errors + if len(message) < 10: + return False + + # Verify Sysex header for a Kawai K5000 program dump + return (message[0] == 0xF0 # Start of SysEx + and message[1] == KawaiSysexID # Kawai manufacturer ID + and 0x00 <= message[2] <= 0x0F # MIDI channel (0-F for ch 1-16) + and message[3] == OneBlockDump # Function ID for single dump + and message[4] == 0x00 # Reserved + and message[5] == 0x0A + and message[-1] == 0xF7) # End of SysEx + + +def convertToProgramDump(channel, message, program_number): # ❌❌ K5000 replies with [f0 40 00 41 00 0a f7] when sending from kk, which is "write error" + if not isSingleProgramDump(message): + raise Exception("Invalid message format - can't be converted") + + bank_number = program_number // 128 # Determine bank index + patch_number = program_number % 128 # Determine patch within the bank + + # Determine correct bank byte based on the bank index + bank_byte_map = {0: 0x00, 1: 0x02, 2: 0x03, 3: 0x04} + + if bank_number not in bank_byte_map: + raise ValueError(f"Invalid bank number {bank_number}. Must be 0-3.") + + bank_byte = bank_byte_map[bank_number] + + # Reconstruct SysEx message with updated bank and program number + return message[:7] + [bank_byte, patch_number] + message[8:] + + +def numberFromDump(message) -> int: # where can I see if successful? + if isSingleProgramDump(message): + return message[8] + raise Exception("Can extract number only from single program dump messages") + + +def nameFromDump(message) -> str: # ✅ + if not isSingleProgramDump(message): + raise Exception("Not a program dump") + + patch_name_start = 49 # Adjusted offset to skip the leading zero + patch_name_length = 8 # Names are exactly 8 characters long + + patch_data = message[patch_name_start:patch_name_start + patch_name_length] + + # Replace non-printable or padding characters with spaces + clean_name = ''.join(chr(c) if 32 <= c <= 126 else ' ' for c in patch_data) + + return clean_name.rstrip("\x7f ") # Removes trailing DEL (0x7F) and spaces + + +def renamePatch(message, new_name): # ✅ + if not isSingleProgramDump(message): + raise Exception("Not a program dump") + + patch_name_start = 49 # Adjusted offset to skip the leading zero + patch_name_length = 8 # Names are exactly 8 characters long + patch_name_end = patch_name_start + patch_name_length + + # Ensure new name is exactly 8 characters, padded with spaces if needed + new_name_bytes = new_name.ljust(patch_name_length)[:patch_name_length].encode('ascii', errors='ignore') + + # Replace the name bytes in the message + new_message = message[:patch_name_start] + list(new_name_bytes) + message[patch_name_end:] + + return new_message + + +def createBankDumpRequest(channel, bank): # ✅, BUT sends 4 requests due to time out + return [0xF0, KawaiSysexID, channel, AllBlockDumpRequest, 0x00, 0x0A, 0x00, bank, 0x00, 0xF7] + + +def isPartOfBankDump(message): + return ( + len(message) > 4 + and message[0] == 0xF0 + and message[1] == KawaiSysexID + and 0x00 <= message[2] <= 0x0F + and message[3] == AllBlockDump + and message[5] == 0x0A + ) + + +def isBankDumpFinished(messages): + return any(isPartOfBankDump(message) for message in messages) + + +# https://github.com/coniferprod/k5ktools/blob/main/bank.py +# https://github.com/coniferprod/KSynthLib/blob/master/KSynthLib/K5000/ToneMap.cs#L27 +MAX_PATCH_COUNT = 128 +TONE_COMMON_DATA_SIZE = 82 +SOURCE_COUNT_OFFSET = 51 +SOURCE_DATA_SIZE = 86 +ADD_KIT_SIZE = 806 +POOL_SIZE = 0x20000 +MAX_SOURCE_COUNT = 6 + +# possible amount of sources and resulting file sizes (not used currently) +SINGLE_INFO = { + 254: (2, 0), + 340: (3, 0), + 426: (4, 0), + 512: (5, 0), + 598: (6, 0), + 1060: (1, 1), + 1146: (2, 1), + 1232: (3, 1), + 1318: (4, 1), + 1404: (5, 1), + 1866: (0, 2), + 1952: (1, 2), + 2038: (2, 2), + 2124: (3, 2), + 2210: (4, 2), + 2758: (0, 3), + 2844: (1, 3), + 2930: (2, 3), + 3016: (3, 3), + 3650: (0, 4), + 3736: (1, 4), + 3822: (2, 4), + 4542: (0, 5), + 4628: (1, 5), + 5434: (0, 6), +} + + +def parsePatchSizesFromBank(message: bytes) -> Dict[int, Dict[str, Any]]: # ❌❌❌❌❌❌❌❌ + pointer_table = {} + + sysex_header_size = 8 + 19 # SysEx header (8 bytes) + Tone map (19 bytes) + pointer_table_offset = sysex_header_size + + # Each pointer entry has 7 pointers, each 4 bytes (28 bytes per entry) + for patch_index in range(MAX_PATCH_COUNT): + entry_offset = pointer_table_offset + (patch_index * 28) + entry = struct.unpack_from('>7I', message, entry_offset) + + tone_ptr = entry[0] + source_ptrs = entry[1:] + + if tone_ptr != 0: + pointer_table[patch_index] = {'tone': tone_ptr, 'sources': source_ptrs} + + # The high_ptr indicates the end of the patch data area + high_ptr_offset = pointer_table_offset + MAX_PATCH_COUNT * 28 + high_ptr = struct.unpack_from('>I', message, high_ptr_offset)[0] + + if not pointer_table: + raise ValueError("Pointer table is empty, invalid bank message") + + # Base pointer is the smallest tone pointer, used as offset reference + base_ptr = min(entry['tone'] for entry in pointer_table.values()) + + print(f"base_ptr calculated: {base_ptr}") + + for entry in pointer_table.values(): + entry['tone'] -= base_ptr + entry['sources'] = tuple(src - base_ptr if src != 0 else 0 for src in entry['sources']) + + high_ptr -= base_ptr + + # Calculate the correct data region offset for actual patch data + data_region_offset = high_ptr_offset + 4 + patch_data = message[data_region_offset:data_region_offset + POOL_SIZE] + + patch_info = {} + sorted_pointers = sorted(pointer_table.items(), key=lambda item: item[1]['tone']) + print(f"Pointer table has {len(sorted_pointers)} pointers") + for idx, (index, entry) in enumerate(sorted_pointers): + tone_ptr = entry['tone'] + + print(f"Processing patch index {index}: tone_ptr={tone_ptr}") + + if tone_ptr + SOURCE_COUNT_OFFSET >= len(patch_data): + print(f"Index out of range for patch {index}, tone_ptr={tone_ptr}, skipping") + continue + + source_count = min(patch_data[tone_ptr + SOURCE_COUNT_OFFSET], MAX_SOURCE_COUNT) + add_kit_count = sum(1 for src in entry['sources'] if src != 0) + patch_size = TONE_COMMON_DATA_SIZE + (SOURCE_DATA_SIZE * source_count) + (ADD_KIT_SIZE * add_kit_count) + + # Confirm calculated patch size does not exceed data region bounds + if tone_ptr + patch_size > len(patch_data): + continue # Skip invalid-sized patches + + patch_info[index] = { + 'offset': tone_ptr, + 'size': patch_size, + 'source_count': source_count, + 'add_kit_count': add_kit_count + } + + print(f"Patch {index} info: offset={tone_ptr}, size={patch_size}, source_count={source_count}, add_kit_count={add_kit_count}") + + return patch_info + + +def extractPatchesFromAllBankMessages(messages, channel=None): # ❌❌❌❌❌❌ does NOT work, problem either here or in parsePatchSizesFromBank, have tried at least 50 times + patches = [] + message = messages[0] if isinstance(messages[0], list) else messages + + tone_map_data = message[8:27] + tone_map = getToneMap(bytes(tone_map_data)) + + # Pass the correct slice including pointer table and data + bank_data = bytes(message[27:]) # Correct slice without re-adding header offset + bank_patch_info = parsePatchSizesFromBank(bank_data) + + # Properly aligned data extraction without adding extra offset + data_region_offset = (MAX_PATCH_COUNT * 7 * 4) + 4 + data = bank_data[data_region_offset:-1] + + for patch_index, included in enumerate(tone_map): + if not included or patch_index not in bank_patch_info: + continue + + info = bank_patch_info[patch_index] + offset = info['offset'] + patch_size = info['size'] + + if offset + patch_size > len(data): + continue + + patch_data = data[offset: offset + patch_size] + patch_sysex = [0xF0, 0x40, channel, 0x20, 0x00, 0x0A, 0x00, patch_index] + list(patch_data) + [0xF7] + patches.append(patch_sysex) + + return patches + + + + + +def getToneMap(data: bytes) -> List[bool]: # ✅ + TONE_COUNT = 128 + DATA_SIZE = 19 + if len(data) != DATA_SIZE: + raise ValueError("Invalid tone map size") + + bit_string = "".join(f"{byte:07b}"[::-1] for byte in data) + return [bit == '1' for bit in bit_string[:TONE_COUNT]] + + + + +def toneMapToString(include: List[bool]) -> str: # not used currently + return ' '.join(str(i + 1) for i, included in enumerate(include) if included) + + +def toneMapCount(include: List[bool]) -> int: + print(sum(include)) + return sum(include) + + +def toneMapEquals(map1: List[bool], map2: List[bool]) -> bool: # not used currently + return map1 == map2 + + +def toneMapToData(include: List[bool]) -> bytes: # not used currently + bit_string = '' + for i in range(len(include)): + bit_string += '1' if include[i] else '0' + if (i + 1) % 7 == 0: + bit_string += '0' + + bit_string = bit_string[::-1] # reverse string + + data = [] + for i in range(0, len(bit_string), 8): + byte_str = bit_string[i:i + 8] + data.append(int(byte_str, 2)) + + return bytes(data) From 853df0fc70aecde45ce500debdb27592d10e395f Mon Sep 17 00:00:00 2001 From: Christof Date: Tue, 25 Feb 2025 01:40:52 +0100 Subject: [PATCH 02/13] Adding the test data --- .../Kawaii_K5000/full bank A midiOX K5000r.syx | Bin 0 -> 104016 bytes .../Kawaii_K5000/full bank D midiOX K5000r.syx | Bin 0 -> 90800 bytes .../Kawaii_K5000/full bank E midiOX K5000r.syx | Bin 0 -> 108768 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 adaptations/testData/Kawaii_K5000/full bank A midiOX K5000r.syx create mode 100644 adaptations/testData/Kawaii_K5000/full bank D midiOX K5000r.syx create mode 100644 adaptations/testData/Kawaii_K5000/full bank E midiOX K5000r.syx diff --git a/adaptations/testData/Kawaii_K5000/full bank A midiOX K5000r.syx b/adaptations/testData/Kawaii_K5000/full bank A midiOX K5000r.syx new file mode 100644 index 0000000000000000000000000000000000000000..356fc2de3d4cb3c9d4f59897908d64a8c972af6f GIT binary patch literal 104016 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 TYLQZ;zgE&mYVeUb|L6Y!H`)5s literal 0 HcmV?d00001 diff --git a/adaptations/testData/Kawaii_K5000/full bank D midiOX K5000r.syx b/adaptations/testData/Kawaii_K5000/full bank D midiOX K5000r.syx new file mode 100644 index 0000000000000000000000000000000000000000..0a74f8e98364251346d30cd183073d419c9f1373 GIT binary patch literal 90800 zcmeFa2V>hfwl)lxIEhmyZIU>>IdNPiSzRLC)JXMiHTSp|^^U*%{eWft!05mzq4xFr3vVmEz!ey?!b6BCnTHoF0{qjt)<^&h}6L+3T;| z_kZ%W-8kLJ#eA}9>izTks2&Kzw;#V6pS3LbqM5CuO0C)M(nhU(c6_kAy-~=mCS&1% z;#pae9afWJZkkFyDK5PvRLbe&21b2`_&lTY`X!AQ=Z*T69^R~X_3&1`yN9>wJw3cb z@9p6wy|0It^-2%FsQ35q%lbeMzoHNJ@NRvmhxh8kJ-kmJ>ETsAj{;DPIsdqjRPGTtU_=n|%#$!WLMK3>mlQre}}Mjh|- zlP1$8l-8P+Rvl+4t(ME>QnOyKw5n~UbQ;ama;0=sDz+-MGHub~_WCz=MYGe!XQ|s{ z3~hBAl<_!Uuh(nccDq5Fv;w}-dYwz-ZQ6D7I(JXJ(dOyl%{p&SdaKTN84ulM({YM!4m_~{rgc;0!q%d)^=D$ zZ?svZFLoNd);Bx#t_FfetyZg|WmgAo%7|-sO0|0Rv|8!bH9+z7|Cx`Lh7!`!Py(ve z(w4+ga)sw8xx#alFrJ|Vl%wQIJV(hDo}=VSdXADS@fu1l@2{cclGjjj$!jRNcN)%)y?HmGH~bCC(l>%l{|X*mE3ZXYd6Wl z0=kG2`lWlvC#CgDkq(fd>tqmG27RJ`)Gr-Kp zAP+r51}=KL*9Hic1VI$~db54{d4rTCDjnW@ zA)P_cJtFNH*^zy|I~n@j_dhta>-UU}_@5lXhPPzgnp}Mw=(!7}l~$u#F4Efm$>HwV z1`1k)M)82wcWL9~`~)Fr4>VdGT0LxCo~s_U>Xk~RUOc#x5O2Y9qt))X7$u+a`2+q-U%b7KAMfKWef+0B-k|X-;(jZizwrBC@Zx?K{MWwp zQ+@nQA3xW}>oh(q?w8g7MZW*~eqP+~LjG)@e33sY?oUPjFL?2M7yM`K`U_r^?*;#{ zFaB*G|Dun7(#PMvKpk&||O3pC?B>h4Oe=GbcYWh*p&;K{0BumK0>tj>;iD$R( zZjxuWZ_bmc$8Xpe^6g(w`nfeo1~9ri)T{H@03}3S z>b${$(VBcV=v;HHX!?18-0s}&JY^%ieAF~Uu8tem|3K>Crrf?Fb!g!C7&NK0Cb`e0 zj&?>nPpubHFEl9|jjsHGROA(d>KCKYLxaz2^4)7z*)agAhcdLVHRPaAAynkGN9x)i zNFDmbOJH<>Fgh|cn33h-YxZD_NsWCkH8*&!^{Yf8#*quP$L@YdyBQu@*}elId7Fl8$AfiDWt&kEFv1f7BmUBVlhK829;oVNb{t zSW>--Jb9R9rStN1+sG1PnWt*r`(W z>N<9*87xNhr;d0r-w2uNw&k*`l`);#5@&&W*wnRnYSH7Ijn3zqYAHU~F$FrYYEpk_ zO*A5BAzQ^9X-1B6rjAucreH04<~8paqs8T>$8cs2Hdjh(=90~GlCB5ls>V3-8@27~ z2LNbyqWFw)~Y&slHrDNe}JRS)~6S1H-5{m@9k!U!mgu)?J4Fy$i zFy!~rfA;$S5I8PwD}}p(R;1XqC7M7v7!s#>H@j*=>sI7-UY z$x+gzc8-!JwQ-cxsl-uIrV>Xy*c4JDF5iCsepwFS!@B{ra>%<`+D1hnia0VPhLq!Kb9KP83|j2Tu| zP;03as56vw?LbK>jpqs#qh}doLjp=FG4L8ns7*r&l{A!4vxX9sw}uiaRtl8-nBM|Q zDltn}0!pe03$+I73?)?3P=f0lt_Dzo?(F+(Y_@oeY5IGEH$VSQZgI?b+kk7ZTAchky*^b-*m1y*HEqqgS=cHu`->)KQDp1 zyj&-VonT`8Jg~&9Ezzv0uWoKh+l29iR!bY38=KoZyL)@P-?q0fp_*AuCE~GIG#Uwq z0)EBkUS4$CE%W-B$**5Nef;qL{kwN>UcGqs`2H<6(o#Fx+TPm!w!43Du)p_hXKTHX z%dV!9i9|dei$+30zv^`@%TAkVer|f|>*w)LA3uKh00CpaKe&V6^WtsQ|E1gMv|Fua z1Cs;QDuxLDWua8XghRD>xRZ?pJ8kbrIB*vA)? zUGiI%t|XmWRcE{vl6LH}-P?3XXHLH@Tuq~VJw~;um<}Bgd$bdAM5P6z(o*NEcpeYR zyDB9dYG>J*wX0I58Md?}uO-?pm=3H-$zKkcYEsk`Z2D~^NsPqMhmz+EC544+mnfmF z^6BYWu~I{AESHLBr>7@Jhx>rc?%w{v;ql4Q{x0hN=K30HKMR>mW_2~4PIDl{qL}ac zC$H~^$G?Q{c3MsLK*h6@7C~3u{5<^Kdf?pRX=?avz5>6^mLK#XrNoPr*q%BYaT`%Q;jygk0XA#$HC_#FM zujA1-7)m;e&SQp>S_V2DD5)t<%23h{I$HuI(2b6jR?2b2P|^vS(6ZzxsWOywQ&!4Q z((pJbLrFb|wu6Qe=xB}-%1}aGxIa)*_u#Fdp@copLaQK^8A{q22iiiGXen;%FqE{G z?P!ZwqBJd48A{p;t_Mmg5u67~DDufr(qa@Sp+Tu3P(m3>>PwayLkabmTMQ+g2znyD zD4`4`G>_|nlBQy(0wq9^RY3!cGL&>eQiY+U?T{!#Nj+_CGnBLyQ;VU5MjRUqB^`C1 zJ&y zGw5AeT9B5V4%z8&+O1ZHBssL;bUJKSi^*u1oq;Nun3(uFIXUs= zr;k5n-=>~LpN1Z*4}AAm?k+uY{kC#_<=*n`rMru_7j9Z_+HU!-yKb%AS-fE%wcfA~ zSZ*wiER33mM(l3(TVT84v*BcbnFv1l6ubXVG&*J^Tyxb|K9qP;o9+QxkBinond$>{Byyk#g_+&`2m z#;z{yGlw8`NZOFZ?AidA`WqULT8sN9rGB`Y*K6n?u)MNXtjH_5+xS9$W3BCryk55B z|3L}Z&P#w>ecneq{)KDTwR^P$K6tN6)kopXro#^D(bv zn0Wnol(cQJP84lbymzR(B|#e`XuRn)bPKKRWTW;Gtl)FLy#Asp;HC&!rfRU z1?q1_Dy|u5vlb>Gab1%78j^)NeBErxk8!LR8&SZ0gROKuh8}m6#!3-Ab?i`gF@fvT z4YjW5s3YFAmi?^N&`Gy_NSE!ns$Rsq>MKRo3W&CsPZKz=(9N2R`(PZal+;lvPlIRB zf;NB4Uxqw;kOqqZXvt7J-b{lAn&Dbdj}eWgyB0;yJWa!u>=Z^c8cNZPc&Qbsr;+~* zO;p{G8*ll~LXey4th9o2ceWiQ$Qt?w zd%Ji6Y^)Uk*EH)KN6<0Wn#Qm$mohk{|1^b77|+Kq`oCNbn1LQ7qhlivo;r+oLxHPO-whL0g(e+kSw*9S5Fdn*+r?ualqZ)u#H1R} zrD8^r+`ihD7~oQS^O}cL10f4{E~H}gk4u%&e)xe@mRC)xiG2QUgntBsMYxI`gvpVv z0KW@k(iRLzwWC4VQKvP2)bUL_wDZNr?!junI{o0v_5Yn;C1&q}e%CMhw=4%T^lwB7 z!%FEi7ya_z35_ec{)LXeB|6UEisT8bU*5l**1yzyp|!NNlaD`qZ~fw0KQ6DIo*XuR zRzKgJhko`A6|QPh43f!L)(k?S@jmHgPj`^$>bg3c0FovAV;)Si{dgl^%!6^My)#z5 zef^t+46^l&xAvd!>A5DwebCo8v;MOtwZ~2c{pW$g_j#p*ai;CH5rhH4WU&78WnM{t zUWJ{?8+ilcfuH-VQyt{mO6wsV4ZVb&3LXdQ8v3_LPqzIV%DQ*WlXqoTGE1vV>BaOy zY9T2nq_{KYz+sQtA~?cUc7!b25j5laHYAkyBi}+hfo=b$vhG{+6g+u%-j!prmbok@ zO-|xSVAPk%)Q*^g9QGd1>M>fPCs-6(q0;GSUunbLmVf6;Hr?D;mbo5{^(H=ntrVMfIs(#pA=_ zb+g0c(hm0u*K}Rl+RU#P*0T9bCY6TyRw5RMT}zC~329P-YC!ShfFvA>S37*3E1I(1 ztevhctex^Dw*PDArD!>JY$tRoDn}_O_~@}+Vtj=0Q_*rn#D_(En8iy`IV9qPB0ebM z1LFFCi1&+lzlc{wyei@q5$_Z6J`wK~yjR3~1n&{?Zo#`nyi4#aB7TMO$p5m4Ul#F8 zB7RB4FN*jD5x*efWf3oncuB-dBHk(DF)_x=&mrRNBHk|IZ6e+#;;kYcrdK?F77=d| z@n#W^Auk?p67d+4;=hkk#LtWPc@b|A@dgo(p)G#BPQ>d({G5n~F%Z+p7!H0nVz_vy_EWhOWi7a*MBHA1F6^1jm06XU*y3B z&azY5A6Ac;WH1iaj|W9wAFjdX_5Nys8Ikh-^B}ao%A()0B&`2@jZ1xqH_v3>tTx^@ zSNhK%pq~l-R^EMBFVshM-^jtW-RB4Qht_w^UU$FJDWOV|rJA;S4pH4M?ea0b z^6BvbAFce=Z~mM2jEDm$vGt+E)Q6G}zfkgPlDB)HU8!^>oZT>dGJI} z9`o^zJXpt6Ii9hJ+)}f!X-%FK;#te{lWv(1+Qc4eeKC}Gtiqr?J|I92YhgN&~ z&pfX~3;lUzUWcIdujF+E{paVhveYB>GOy_#sb{PNzL$y?isl~(Gi|H{U`bnxeK5P$ zTx;mA*Hy$fT4x_Q$L zm>i$V=C*Gze`6MK=@bh|9uf#w)pXDIt`<64Xv6xm)qweDy{g%Nmd;?Jc5rl7(n60t zD04xVW#I&dd!;fA_iAiHuXKvpP3pxM4`!FCilwM*)-=W`>?Ir8c|VU| zrYh$#+s&zmCTlEynMQ#LHj5ptVMVXxq2V&DA~3resKdq==i|+5plWBSj!6s5N{6a9 zSbTG}eG@d?R_gG)B}H42+AUBgO*ZZ@o_ZR0L6@kfb`Lb%QX6p3g<0`j=K-kF*l9fk zjnQQD5oorP?mPyKP=E7x(0ohjJO$0+`e*#Sr}i8)L?g`?px&mx^Aa?S=YIu?+4A~p z&`3vVz9n7QO}g9fK!aU(^F8TcHayb$02=D5^-rWNE!13%ajdrYcHFJcpkeB%e<4le zFJ1pi8j@UhH77wAsJ}Hu>bO2pod&hiM17Xjzz2$R1Qwd@U{wbSD}aWcR3snu*XOZl zbBU^DBd7=YHIb5J>#o+!qzHT0<%ShB2&(}bIg`BIV9gGiq@Id{oFX5ox|5tpv971Y zsE4MDPI3$jw*<}~AwFGUG(lI(Fp4L?Q1VkKk-T_!040)YV>B+sICa7HQ9JMF@yn9R zc~y#X>XDKr7QZY-jf}crLx?t&$7>#F@G zXxOfpIZA916Gw@Zv~ZM29t%f_rEN=Td(4f<8eGgP9QpENF zG~`sxpMZ)5lgly=RCpb3+h@?QbWjKZRNTIls_BxZ&?OC_OJJSPbxBR=lA6#Z zu(Id6q#|@lS?H3o&?P0IONv65oC#fmRRGW_&rzE+U2-CH$uZuoOqU$teoU7f30(r~ ze6C9lgf7_^x@2GIl0BhIc7-na202WZ>% z)`Tu82whSTx+E`jNlxgJtk5M{p-VDCm#iWmOqZmEE=dVp0?Kts67fuzV4gwKB^bVX zcO7#}&u*|a)f1!l@6VIRk1pqymeAyK%8MkieHcBftQSjc>}p^TZC>&xo=?zPBZZ;w zJaR5xwqjvL$>x@bbL}ALl-YRD09oE#Ca>6l&{O;q*k;}vk#Q&ae3U6VxCA@WeyOtY zQYu>954hB=gA3ctCzRH?RA&FdrCyGRvAL!1qz*9S4H>I`BmD-dSM*9r(bF5C9#HrbQ;M*S98u;Jmoi7^K+ix&lv(hzbQbg!bVQi}KSO6g zPe9N4X=PeDR;IyEBYv7*@>Afa=#(<09D$C2_VSb9C+VazsT_iifcElV!GEP+m9NSH z=m_YK_z7h~*;ghIKY{oOOsM=%`7gNs3;m*eQT9MbK>r&58R3P1^r58O zhmuksN>2Mwa@>cK<35xe^r2)I{3R-;m7PA6Z1%YRkY`cRVZ<8$Dz zq9WIalFXm*t3B~wlvE!|l6`#QAM&4-cppllf5Jyt{J4WsSE}^Z;F$P2c5?J`c^gfm3YgfY<^Z*!m(D2~%dRTDx1=?nd0s*umgoE?? zkF|j;!0?|3SpEyEe~gug<}Ppj7S?+z{a#6mhm45yoqi9@-`~*tY2pPY| zETjmtkeax5_pXUNe#~33S4MbcvT|H5t{-o1Z~fE=FKZbV?&=0&Lo1gSQfRTEWw?0R zGK8mqkyDvIlDF%2x7Las8A@%THH*MK1*{IgdhkF-8}_0VTOe|;REQfIW6i?z_9mp> zIFv&~x4mb8)Kj#LZ!E(1`3q)@E#aT64nKX==YQb)ykaz(c?G;8b@(6WH8cF;es5mW zKgjFg+10$F#rZu2y+k>E>8kJud6RC_g=rfv$?*N z-&kAUSj%Ct(rR{fHJeJIDUnWQR}<-Y?0ls7oCd`!4AZk6jN+oF+`!-~h6&D&_xJXX zFdB=@9bnZozG;EAEdC&1@U}m&1_%phXBuVSO66*!(PW+S@_w4F!%5RDW;a)9oU(O5 zSrlz(hK;1gXz(rLv3C9yTknH(?--w?gM1c9o`(eyfEQJc!RD9g~x%u2vTEasN&N>C1GrmOy-s=4k9a%TJ!m_0rXL z7UwtH8#MWxN(PJsuYP1ByQy||3?A#LshN1qsK1f@ovrsN)R}y1H`qyhX6d4}H00;$ zMwjIyjOWQB{TlUmvoF|lMrq<1%Wtlg#(e{HqpZo%(m83N!JZ>RGu0F?kIiNl>4J2H z#vylwX1kG>a5YJT{5ikTZ&jEhLR}c10~%I&NGx`kPhW=mcsQwNsB!vP;!=g%=6Vw<9UFR zRuK1NC}GbDl$@!!4k+p55D%0za*)qZ;>Pt1C1~w%l;ltj3?=M&fs$GX&jFNFSbCtO z8$kI1B_~P9VJOKxACB@0r8n4*ccBqoIK6^|)7-7Z)Ta{3Y1!b{h{?tHokA8Rrdp z-R#U1bHVeDGPQ~TeaDom2$(7D32+$x2Z2wEijT7r^U2#At}1S(x#51Ne0EYU*Lsuv z&pAnvRLMs15f=@p$iy$rfC=bjESeny>iA)R+x9fy33 zRjF@%Ozv0py-LuZnNameKdXFE-ymM6jH}P1jDAwbme}c(ET(|hn^Bd0V zlsD=l$k8e9)KT1*#ozEVxgXRKu=3Ku-EYbL4R|>i5}$N=l7XWZmT3fs6iQ3Nr+M074nQr_(VDr<53PaMj@Y5$agY+ zp8bxXcn(kp%MYlB-JhQa?@*9?O^%yj$ovRvCjS&A6mv_p(kZ;`eA`^hWz(rxBpg%~ zk85RlX%TNGhuy}pVP@!oyYkZ)vB1Ss1>RO-%#sRpakfdQ;n(vzuKHSui8+n z^b2~`1bKQ@r_Ljt-Zkf*LoL*+GwKwpJ-%u6eF`X1r_|S|1vBbb^(|_FP6bN1+%M`Y z#Ou^?^=X>XPwF#PyVQ^BBcATP`YDg=mfm^p;k-_Lqds7$Qs1e!a93{0t%eSLBoASpI<$mVd?roq8Y8!O*LM^5+3cfZ`c9 zPy*CXt3V0L2h>B5PrVB$Vfk|bCAfah1C+4mSAi0SIu$5Edc6vi;B7IZ0wr;vLj_7u z-UgNm@y2DK1kYn)Jj%hwDCBbr`A){;?Itmb=KyuE{D5+_^7F{AgFTHV$4#(?=pp*z zkDuSSKfXEo*gkNFj3{vE$3G{??c11Jkngmp^@OgnS^d@eQEYt04yY``p# z{Uxq|^lKyJMW57x3#lv(UVi7EaGBOWmm2An$}FTHZ4oP>h16GEYTdIq*PqvCEJ+4- z(S+yKi`S|=oCnJT7x123wTqSs>p#=O<75`oCi0|Z-1tHNe(ufmi-~9BPk-uQ*SG0O z?WDfGm)wkQMAjqgv8}|;>R#q3e^fYLJ6Ss}9A$Rm8=7-s=y_@VqO1h| zemWHmYE1czjTjzgu1#QXs2kL@H};)(9JJB=C&kmQ?+>4Gfpq7{pFHlaJL-FKfNX%6kpTNka^Kak2snJ_W7{2OmB z5*XNjW~=*dH%kLT>MWDGe<79a;?z60^n<*z5it{*rNcifwE(GFURUtuF(VryZ~=G- zDh`4ZsA(8vs=+Y6FTg?`*87;MK0Jio5qH?k$DhIhuWQL+F?=0=_4KzJLs~2NPtpB< zIe{r9N6^xzrlcHTo3!Q;! zkhm@+$~7dOBP91;7Zle8#dSg1E$(-I|Cr#l>w*{ZwBPGe-g&$q=`qG6{kNlJ;NOog zQ$kiaO5&o_V|1C9c0^v%XnT>Rq|Rs&+n^fZ^Mgx zDB^yKcn(F}PZ4qzfdUnatXY2j7gR&=4&kduTwmaXui@2jp`! zo>L8F0of(et@Nb#i|6pmP9gXFK3ci@1={?gT+V4g{2l@MT;8I%p8YQ5BO#A&KYmVL zewu&&?9nszn&sJt-*mG6+vX~h!d*E$+^cREw|>s{gOCCFjy#kcNfM@B#ozgS z4LgUEgO~;SCR4J-eri0PEDsJ=G4vHMz*VQX_(Srlz`bub@ ztmGmT#f&D&5NR(oXM9Ym4*yqngnW&mc6oS&<(&nV0#Ai&Mkq`+a9fs5dWxu>CClXd zXY-ce>aF_^pN_qL|LM!TReJLB4dO1t=$*%$8M<}v@zWRYK26R$R)U0V;i2LG?=szA z`8G_k(4Ng7i76^JP1O_H?M?>?{?U8wV){v&-0GRs>S18<(Sp2QVG_%3hqtvaXqsMhk-G;Nz9?@Ttpb8opW?&wT1K%6#538i7w)MwxF}M*Z*+%cyTwqi)!~ zFy4h(WJZ@UJI?4LW|J9}F-y&;1GC1AS}_aFs0p*)j2bYz&FCCv#TlK(EHa~$m_=uF z;W(dcW;hBN*ZY&aNw`9Py%uq9#q*j0|nrbeIK(CDLU8vSjE%!5k6$r0iB}Xm%QMNo{KfE(s{j8=9M8DcBgH#I z)c&8PU6KBro};ZFT^Fbl>Fa`41zO4iC1(N+#{vxp0u6ft1>Xb;b_5ExMeW}dwS7a> z_BB!43!=8?L~YNATAmiQJSl2Q+a?l|@+seiL4%6(Hk8j4F z{r>pj{kykEZ;T8LK@Wc)7#^r}P0WM*qJOvYz^*4yRqrRJAK#YA^&iZ@E;0)#c>!Zr z#|Nc1Z{Eo_D#(R<0IBG9EJr6_E=$9 zWzAX=#mE{m#tzMglqPk^dsQlHam#;2Y7kQI{FzkN;)ZAEpU2)0k32>!sLS+ zeF%$>FXJEIzQ(>}Paghu3;P~d$~%d0GLuRB{ow>`pp&ccbODph^{t)FwT;c4-R+Gm z99=}1S!Ni!l=+wy^vaUWr2G2rW}#5PK4ygiTc9EKYhbH4&coXJ_TEvg-Oj_@lZUPK zjqSbTN?Uswbvn&*si3+SY<7pVvs=b`VfG$sHp-`1WwMDi((BkLs-Uft&N6dRShpmj zexG|~(Kh|^$%uI4-1tTL8~E>64lwfig_2(=`J2{$t)21bsQ-nMU%KSaYst^nrvF#d zkrQ&`(d}Ee$dl(^p5G$RzK+a2d-wk7Fh+PEEc!m66(^xJxwr`bs-fMz!|l@XPqkv3 z7>kXe6FdUH#A{4WO?9u8?EY9+*0$j z78-p%GAQ3*BG_20kSgDSZN;V3m&0TVU1j(QBJw65;e9~m!`pEAEkn`)q~2yyvrH;` znxW9Wn?y)myH6T}Jg@z066B$qGRydRiyIB!O?j8RIpb>*kX>{N(c-pRA(1(Ry)qVi zz->QpOD!N$CSS?G6?7!#_$^rG6m&^Z?lnng}Uwe#(UXO6SVpVv?9)z1ud~(3E$5IJrVRs(0xJo z1l<+%o1oi*t_hkKlvfqVO$i>`GV}Wc1XTp(F7c6$yVeI?5a%6&S_L%;suy%d&`Ci* z3;I^jF+qP9^e(5o?*INodinV4BPT|gUOiZtAGvDh&&~WX%H%mIM^8`6KR?Pekiv9h zVdU!mBz%-fCPW?q_CPUL3R|W_a{V}gO*Hr@6B()H37_F)vzpxhIiG*!`k7fsF{!X; zx;n}v4}Qbs|B#dUW@1tY$i$kQA^IAdWM`}Ihaojb_>;V1NsLgfH}NXKp^aVrP%8F1 zV{@hFd4;EdBJ}g?)H0I_{d}E_P)giZk1RiAQr8Fqm%54f1#WsCc(xGH_vV3iXCcBg zH4hr!#rKYO0-u3n*0f+DtVQFS_J#d?|J4C?o9QI>`HTMj%7IK`zvA1>@5fU%=l`0F z;Dqq(+_PV?>+s~H7+tTS+W*7~BR0Ohh`mjI;Ac(-Jcl_L*_(&?(jXsp-C^eS01eu%51PDEFJm;+fw} zwu3bH!Ce1=ADi1mZZ;OeH@Bp10<_|NOzY*NosDb~o??BGPzXD0dwkfzXoWea^Ljl# zw`nY7-9f*bdANfN zEV#!mN-l_4S%MVjqC>{Id*<}OX}3F^4uOK7AR20}eEtEx;7$)7YKkX^@V*XjI_Lrf zRiArl!C^Da>!v3^e|h)z^|NO$UOc@sbp0M?@l&V=*caLvcB<_)TT?PifmYCRD8U4f zrff%220L!Yq%A4#@YEv`JY-AmwgD#6!qE9u;n^0owBwjN3$ zyNkN5B`G208jI4d!zeAZ7Nv^hoeyEZ@2<4q@U=YpJ;^IAwicZQd%^CjWXu(;KMqtB zXVbahSZM~GhxVvBM7{Qg6mqOIQjR?2xmrO}*&cJi*4pw-ib!fZWvyDXlCS1*HXLq8 zs2Vg@C8H7kam^=Ez?!2u`2}MeX>PtrXW!J)&9&L`{Xo=e5Z`IY`N zdH9{wM>YBEu8OWNxgr(YXA7xph0&$d9g){Qsa}l5U$Hetul!RUz4E2p{NAbzt9E(t zRf+5nDF~Yqp4ZIv{?+%8*w@#`iAPtZvi(Ws(=BgCHSWP--KH!LJ>gPWUO(cWTuS}G z*A=q@;en;VNM3)hybIs9RdT8v4(=`NI5sSKLuNMqHTos^L3!(ay)@>0YI|aSV7Nbh z>+ALLYaj5Id`X_dTf$v3N-iwg&hPQp=^wn5{*|Z2prAG?c!nl38q8RUwYk2Q&!)8A z_^;2uYYD)`2wQ!BQE@p+)bCKzAvJPF4wtt{!I3e=bfIZw-1TAUjWlNceg5&>Z&PI1x#PQVKTAji!PTPdfB1leaio%|6*J?Qb1!9q(4!KeV^5*E$FIqrYy|aq2VcHQr?4RGFXWRMi_fb?Dx6s-OGF z>HN$mPK{GvH0so-$uYrsvrVH;vnGELdzZ2ME$TFW>744d`z{N*qMzjHeEMllJqC^U z8oqMgtJmZP=RR{@nHks8O=~nTt(}hwnx5A3m7dnFTb@E%Je<<< zc{HWv?_f&H-~N<#|Gg>gciWxP%5i5(`@Qz2wBPAa@W&$E*`#(|S>(IS%Yl^esvmjg z`1JVE$KNK%ozawa@A;4BmO|U>o7+2;LyYha4v|5$PSIX1J-qec&h;C&Ny?X+8y_#o z1ET|@V!nnfjD!XXJ+0V*k7#eAx$C+d;k}oX_sH$?*m0h>Vux?vXsa}Cg-W}Rf0F2* z1!8M=2J*d9Gg>Rwekt{SLX*1tW2s3Q5Mu$}GLx_D;FfF5-Mt{H=VuX}k>vRh1snJIW+{O|^L#!sI-{_Wn~ z(V@Sp{hIa4*}>L&Chm7H%uivA?+*iC2#0M#>Rbt8fR*iNhY?ohXCABfvA@FpzO1IU zcX!Kdto5Vp38mJLb7Ps~bV_;=KaBVnU*30{C*M82Td=-VvXk!z-sv49!}spoA|u0t zyiD-?(%8gr(sd_&J|+=q)JYwJz8^CZ>6VjX0=|cu7=K%w@1dXgdE?l0U;Jy%PqFLn zIH_&yGm-8(>4Pz>)w<`TQ)6$4bl*vDv3SsNLCKh&NWZcBXq2CS;G}ab9@N6Who`a65RmyG{Hq*qQl!{okp(kG0*5!ap5w@y0E&Vv$G z?(dj9X+Wc7SbERXUl;VI^npMBZRry`@3<$8JL%XMc_4ju(zj#eA)|PnM~u#nktfm@ z&OetXnOwsQX^K(uMw;R0KY+s0c%JCKL&*TG9#nvm$}f~KprF)#p@gC1?}`!^yd__U zx8w(JT(nMxV>K9ZM^yExl-=Orf=j1-9H(_S`MM}Fsi}#NN^TIyH0I9 z=-R^;J)K}0ZH8$zwzDj^p_G|^Zecu#d7N47(WGG3RGGj8gk8q>)}7qNc#wNHkcCuO zX$;=1Z`L>8GyOcql4zq$gegs7rNN~3=XD5ad!&Be!U-W2Vg|3#I`YaUcG-TlT&k8= zFD$xjecqCvl2gnAeZDDU76`*>gtX80whO6P0E5vUJLY<1yVHs>Xorb8Nw(Rnc8l3) zHX5-UXkKqH=%#g3vs06kZ~{9wJvliwJw@xK;jz2z|3}8Rf6W5@U1xz7@#eYrXiES36MNe{ zCj4`jzj?TohTQ>F$x9@0vbVV%Ef?|T`HAsuZly8e8_3_WT#iIvBX{xU!TasXJK`F% zr!d7#G5CRH5)44Kae`m?@I$x zvH$jYUfI|$lWLFIW5S~370c^#PhPcEi08c+WV>&Cd!T!uoM{ocPjN)cgQ}u3!9jSWSTYi+vo!P&u6tEpiw!2 zJ;iDu5sO6vnAgUxE?(?b#rAH4F^R{Accsha#^fJN<)lT)>BK}IbL1k)61)E&gboI# z*&f;G1Rb;O+Yqp03$`O{ci9ykyh*X^H^M$Pw%Xe}Ew|X|ZmU{6K0M#{>WB2;xZHrJ z`ev zv(!E!ol5u17WfQs(zWJC_!7YGEae59-=+t(Y4{Y7=r;X`^w@Wz{1Wkby4{+F9PB$$ z{R;WmccRYji+v}G?-7sPS-Q93djR`Rl#P&!eJ5&OoZoDJYkq)F0qo9F9ES%F%n8?) z;A;T;PE>w}F9Pg4QC@&2jSSr@IUpbVPSl6dzQ*n>#ZQoreJ2_$AJ})IsYiP3JJFcL zbF+OX-or-$_MNDH!hNyt#K{Dglb~Oa&Pfa9NjwL3XF1c#W3ysHI_x{qc?Y@JccSqc zo{*JIy_8hcLcJ8>kz#{u$HpTP4tslE0F_hb7` z%-}h(@5IqdT!(!p8gq!pz7rJ#ARh{myKq z!>HD-CDdeT0kxX71>9JzyRaa^4ZPiEu~=;m_|IW;+AAx|a1hTbW~WuH?(Dz<3Fg+x zSU7}<@s;Hjx1y?kw#}icc-@%TUR+qj>^Nq-mzP~`x7W*!OSlOzlkt7PkYf7>C^;(D zTHOxN(5&^M9pNe0X z)q0sG+B4uC%~d*y{ATGIeTnZ?ODk4Q>Dz%x=Gg>Gf1~aoz(gYzjLc_x`69fYB~DGMlj|<41K!fGdX^c z$4qAt+7%|xOqX=)SXpqKEhN*Scpw~gsSekI%VBn@F4-v=?DJ+T%%V*5<3|0APWN?U z?!%;R-fo(k_%ds@8<{TZVt}c09< z;PWk|lg2LZIb5GZdba=e8qTx*x3|E1oBqx&c9m!QZyzEZ+kg9n?NZ-SnkR^7c7%*F zJHiv}HqYz`8PDtpPq0rnvm-nr9d1W>g!9afkWpqw$SAWTJR%1Yvm-phuJ+81kez3C zgc=_x9wGnCj*y*ac7#XBAKQzG@yw3!7`xXqJHlh|w(e?;QD#TTD6=DEl-Ut7%IpY_ zaUW(!cuZ>Cj_?@I109Bq4bHe7Axp>X2-$gNM~J0$aX7FWg$48LS6AA`A9&k%gzaIp zya-ddg=Mm|yMDB>U1YuTpBvvEgl9EaF!xw#uwJ=*+1`Xz@U0@+o2kQm&W0u;T87UE z)(d);(C+)}Lef>Vjfc_1#G&<{|3Iq2b14Rl#PV*O_`mx{*a zAeVX%?}&a|9rB1QA==-}J-`g%T{PA4PLcnq5eMq#-KJ?i(V zfq>#sgMQDlPxX1171if+d41lM6~*UX_9-5=o8Uhen9U?M4`k!=Y#(DNb7tlSpB>QO zXac~n&0?C@qqjft`O}AY7@B|n^vRRhd@HWh2d9&hfo`mt)E^^0au%{x9g%9Jn8S5SJyOntCYt_AzzSNAl@qw{R&A@gVXWF0YVJw^ z`HVEx(*)8*no2PWK3v?Xru0>Nuo*3T&3m?JWx3*kRb#NeQdxubrl*>&2j;4_cu6Tm zZ3pIfJy+YXR-O4yuIPiD)%Gg#Q^mU@+hF<6HI>r{tQG@hwdS!@?JKo(BWkJIg0<{H z3^aD?F0=eD*Mp5D?!R2~lv28?(^bt^LMWebHCzcpUaG!Qiz8j2|cD5b3x zDO8D`WsD~hY*;HXJm(_yR1@=6Tascozu79@scJc|ubLAb|4{;bp%twxAYBYC2>&$R zg>`!+W~oTAy1SaNRIPzlv=PO9gB|>SxL>sGYPfY+|BP9szMZmPkfT17w5p|({q61b z+-eFvvuG5X4F&vc6KvL(!j`|x)Dd=+eB&Wl2Roc-Gs-gTj(L#L?kk@j!J#r=2Fsno zti$sCe5+q^0_5C>%*Mth4;!$N-`vRma}dGR-HScN_W2S+*h!=ku}D8k*!!y_1KfkY!nlc8(6r4>PJ@bM2&H1b)U`)!E!=+j@5y0|_* z?Vn2-cMM@e%$%BwPe(s*>0|TRnSddvi_8big&D6dHybzlO_7P*T+kSq-IxjKBS^P7 zo!4*Z3zHdR(iEQZ&86{~nNQ95bfLM`nb4QDY0pgDl$^|%bF%?MeqNbNOvdz!)9ZR= zDmuS9vu2L!;&W@WOFG|lTCYyU3=w10ted# z-0DPNI`Y+xXN%8y4B@$u6?vNrOy_13b5Y&0G2=X&4b4SoHl{O^Swrlr-w-gSX9{y` zQvpLjpE9UlyavB<%j7esjp@1gSO0Xx9G{BJ2If|0!;{IG%;(6Qe?B}Pp7qbF^MSd* zY*-(k&FI5l6UKl!IJY_LpK=&i4H0WnA2DssZBA!Cr!7fC&YYaio7d;0shBaQOY5WK z>G{QJA4(uQmojDbs@*-G&?QY9li{y!OI{b#NA!VNuOVy+8dO7YcEzO5_zkQ2gg!r; zn>^CzKf?|X4o>fFZQdE7kzMD*g$J8=2DF*t*E*(87f|&b4w-n5%iG1%lb@d{W;$=^ z`+b3A`=!piPKb96y6QIEm^Qd3z5asfysQxrdxU=yM`MqT8Dq`hr6z@1?a|L{)y~Bz z6CA7lsni@asV0@bBl>KvAf1YX$ns6Xmv{ctIQ=gckR&cN495nNl1rANpzlF9KtH?E zQWunfPYBA=5!abi03E}5jbD~BSK^0pK5~U;*K6q*#U5Ln|9Ltf(ra`e*-M86e@)O4 zL2n2;D(G!Np9uOw&@Y0{32GJ8A*fkUgP=2leirntpf3e|D(G*5-V^kWptl4)&);>y zpXdMlc`l#xua?(eOivb>ChbSbz*UrtqP_Vy4-6#^ff9#6i9?{oAyDEFDB7%t1uA%Yjq=*V;K%sJvkAR6Q!_Do`}QVzto2EUd7o64(RN%S!Kv8E z@iu(}@Af}2UsJtJ1`51Sx^g*R!&E3SSh3^D2(%JeNv`4aX^zc|3K={m;utRNWH5`eaEG)$%CX!{|5z$k02^R+c17v~p4XiZe=2nhyY;fw{oC-BMX=~<~u`xDA*%;dZ#-i95+Yo4o2I|BAe|z8km&mfb-GFH^;2KuNHLk8X=LBOG zR4`*AD3SyP6h#!AKl}UZ`&3V(pza>;dG9@Y??)SlXL_cmW*DcYx~nTd*Rv$=-seJh zL^`%SAnDi&pgSTR+ZPt&IyNnkbZmvtb&-y(nCrY&5+&@4>)3SA5s{9q6i7O@G8W-F zwsIEcI<^Ynaq1vd$u7B$tqMpwwrc3YNXJ&gLR`nD2a=BMJ9J;9W2xuY>w%C4T{qXU^{`W} zW9tPfzPcx+J&#~vGCPB<~p`1b|idtkA$!8 zA+Cdbbq|EE?g7`a8Q8w?)wN+i`RdxzzPeG0rSTpvf%`XfGy$K_<301ZIj^`Jc(9Mj z`x^xg;OVkYKDql(+_Sew0bA*KvT8)Zln7=SgCSTtqA1(=H|!p%&{lx511p!mGQsa-TmZ+TumxrF9xK4r-t;}R%+Sbp1C zMNMf_W8qLS+wTg%7@dmp&24oIvux`GsfJ{^5&epHM4Sn#dEZ!r{&8mCxI2ffkv>S} zg2f{`*-X{tl4Mru4`Wh@=4+rKf|iNLhGEp-bU@4W9Q*wIZK^xaGQmPhr6#*0P*tm2 ziE2?u{sq+|{JLkWqhC-(EK2HeBM>I1nh%q5mvqL8JS)GV`qYo_Vm+@$<ZrS?qygmO7Dhejn|k3#KniS%+Jf#U#PU3~+e#Bdv5`^mpM9v>0iCvM1EY5&n!r>`t6*5>D^ z8R89~v~l{-&GCZ&36)I+UVbYa2zzZVpFZ)U-3RzJj3eSBxL+xpODb3cxTLW8reCFw zqto30MNTqty%*d`H~SYc^Wko#3b$b7Ye<)Ud*$03?D-wZ%;V*hsHKvEO9H3%1N4`z z7Bg8O!&1b!wCxgHBA))=5r4l2{$9ER&u>HKXKGfgFI}vxZfIax8Oe&uikX{R zX;4P8`K!cr>?CyWVehneR6*WpHgYm!MYBzebeGwaB!y*SQvmUo3t}OXd6yDR1cwlS8&6ap7|lOp?m;R19WlnrWx2ji ze^Rbu*nlg#mF?!Csw&%qxytOSvRkW0r=+@vOQ-$dG__bYQ9Cp=JYK7h);-Kb+-D~@ zq3+Y2YQtDxS9P3B2W#Ye`NT3-TYGe6skNNdo^01n8W)=6EBA@rE1PO(8W2Knx}pB2 zNuG`Q{GLRl-@E^PZfdCS`vVyt)@8jT{%sWm=?fk2?;W3G`0jQ3;}7uFxs3*I6Ay{& zkk@s%Wrl+f%+uiCW85?@8<_zX?#6MBHsd5mm$7Arwx1g7rwGrC&69+FW7`Dbxv>l0 zO0qF%>>nfy8Ap3K8amqvFAepL3N}@7oM|alu&sdbV!D$NhNkcm1qLUPV+0tO#Qz)U zn=o?pj^k~Jx@T;ToQNet$2t4ary%Tw9tP;>R_NM0 z(L)9yXUzk=?rH4j=&MuC=l_mec4!~aE7uXIR*v&m&GYTvO4#RO zzo$&OUUw)+7KD%&;d z9Jka~jytL?jy4s{n{nJB1S6pxbJhwf+Z{-NKy?RGuE)-i`QG4?$xkjxa|uMVVH*A} zpIq{xxnvY$I9*$5d)<;7GISh3sXE4%DoSq=)MJ!uB z7Z(<)^S=}o4VgC#WJp(9n3qpu(g(=A{Vz7nX5MC|cK1+`&yre_9zh85x!AUJ$?`6) zRC`=L=dk)mSm;aiVk39Q>9p;tOmp*t z|NHje#wlZuF&FY_YD(ZlkAc@md#1B+sC$an2f9Z&_H_?)?Cl=l*wfu3u#;nVcRRYItDx%rYpP2a>5kw4QTQ+2V)?RMhNu!6mC9NrF?I(&3d*}F#QBkRiM^YR zs9p|EIApzCDqrsn$bAS?(uwuc)fPE25Sc6M36GUue~lJhG@evBI=bG&hJU*a{GB5g z0Y4JQ*pCkP_xBKXMic~UN|4{P*?fL+8M(e8e}dR`G<3STGHaL^?Cb7mrMccJ^06<- z&()}LuQlmAk%=$+bf0y)10QGP%d_@&z2FivQ^}7_@zU?$5_2_iNlJDGZ+DM8PP<=c z+e5Tvy*pUt2G^`P&@u z`vV{D$b;tGhiRX~edI*ivN_K$(KV*>@Nj?ab=j<#=UrGfZ`yp3o4W+k<0X)+;3|A^ z?zK<0H|i_OiVL29d?sULt$%XG$Kr~Y|8I5-O>|xj8!D=YSr8;?)#^{CNYbhz^1P>6 zJ(|z?UMTDMFJnzXIsrX5frm0$9Q^iQvsClB=OjNbW<7}$5oBQ)ypXPK)KHt!_Lh2u6 z*<2wKBfJ3DHPeDBc!^tD=Ot~gsrG_0BdW@^Ho=r^rZ?J5InL@jb< zKX)H&FH1w+?Tz1Ssw&HLD5bpUN{Ug6ii!%gUkVB!>2fq0br$QUD|@?0L(Gms2z-R- z^6`SYyK>}84fwCGuZ}L+@#f)nIgHxa)PyR3c&M+7;l))|_T|ryl#M2%j?1Gr5L2Lt z&ScbKf`KEZMu9WBF^)x+&DGmfn?MyWa^4Il(WqF5EFGwA`xn6mq9m!+ zmxp%>Rh&;^bGenEnwO@k=EuH}&y=-r8)71DX$?*E_^Nz#XzdBgH)f)$Du}atHbY4P z=~~%QZi`mUZKB1foUZ64Rb*IPn|!94qhnjR$W7}4N@QfCDr%)#>Pfk&e7-?iknOAA(Pb2eLR^= zr4q>p1V+AxdsZ@@Nhy>&A5#aXDkr*fretV+aI*(4e<*9}7UpQdwVSB@JL__RHJed742sJ}XIy4?#Lbi94t zujr@Go4$^2aa{U-yr7%DjsdYfU5|+KMV!j<{bGOQC8pcG-2S58KcLWgb-ZX7K2r9F z)Ai#Q^)$VS3_5sPiA(0wPbHWHGGA3)E|=-6{hUk2+3 zr1!VKN5$0-{bs;>Gjd@vrUv~tRF4}?(Gy@~$aKGb(b^du z4nza8$(S5Ag{The!srV2-;bDhdwDF7xWw^EoR5x|r%VrL)Xs2r%iB+5dy?vbdl%Zt z2GI`ZQRw;Z{G4u@zU%%J=R;pxp=UHDly)W?r*BNyp6beRcgptk{)lLYy0TxOJH7ob zdpp{N=y&}AycOqt5cnvN?jv2#I}pOazy>hn6oqq1HTzO^ThUY&F8!f80M{@)gE!%z z-?K)MNMZ4hVB{0yW1}O(LxY0@{e6AC$VAfB)!EU}-rm;s^Ji;oOUsWR&CShCO%Om0 z4GlP+m+TJb`)jACXWn}XlaY)>SMiz$o5bVv=D%}sWiBNSyGJ;G1Pe$aYuGx4&WKac z41~`}Pz;Sr%jIPp0Rt#fzyn1I0FCwA#m56Kjdnqeb#7j(jpph%SMU>*dRxxRY5 zLda9PJymR8;EKR?fm;G?0*?ir2=oa&7kDW!BJf(^t-w2h_W~aTCIzMhJ_;mlD_!3w z{%7t34D?`;uSS1KDV$4e>`RUOHQ0SpEvat=mn0B_*o$Dm$WyW)iN?s@gVa69*wfYi z^GAdJTbVY^ZmOL8lFHiprk1vzp$X%{{k?a4eyFoC_Z=ngjW$8Q`^-NVx9hqYr=Hl4GOBT3vsDnBmmwGt79s!C%>8j}4;5+wB#)dNt?eWH4WYjyH# zZOYI_Gn#?Lwe}U`2wW?4f`4_b80$51swqflDgDC0C8MwnmTb*wO;+U8=cpuUjn%wg zlKy8};vVoJIU$NlR*-%um6QU?cWy;f&)D+%q2q+YS_Fd>-iiXoAzGZvf&3*KOVcC0 zKWmD`F)BeLlf685=d$L3rNO`=$QSWdCn zu^eLA!vcq;ETnDZ#7oOAAHw~;ujhdu1$je3q8$W#!xSygJi;@_hbW&fLN}fucRWlT zY`Z&Kn;RA+D_B`tn3D|C({2oKhcJA71C2w*H@BzTr}rBdm3E)_KX@PTWWJUq5=&NT zek8S;+H18W?J&hj{68Zm-bEFh^6pYx*Eznhy19SiL4Q;Uk{CoxMZec`>O4N!u`V0O z`r4|q$t9nv&uiH9klu_OH;@^{=*9khoDs+We;{NT$)e&<{D+ExI;O~D zu2Nq0j$~T=VQ?oIre@h}^ycI$aB~f*^A0*Y|!C)gSqOU%lBhnw*(e|xz9e(J92 zu<)=giHuM3MDY7=v8D2~>EP>T^;zLu;cC%_Znw(O>?)iptQ*7a8RKPEhJcl)dD(>@ z9};nqcZJg#k^OQ)S&gAJhRzPkePX{zq2_jpSg%J%9?W4po>DN_r$Q~{`xrGrvDsp@ z;TCpaYv#pSBl3>@khw3=cg-i#9r$?n0n=!GuefAfRnjc4$a>Yh#G=x7*7M)QC9?dG zSYKRNF}cp|yX%XFvDt0!-Nnhl#)@%ze0=^fdG59@O;3!D&OF=%Jg(!zgZ;gY)j7lD z2!e@caEWbA8Xe-CLQL_ROKdxqm3hPXP;dJ;$it8MbbS79<^Rq!>S9WIRxA&M#Fw+DTNRuys!;7QPHzLh7hBObz`lgxK}3v`o+kk(LRw^jn$oxtKIJ zkbckx5$32^u1ls2J`vIfq2w?%p4b4bl6EHt6XVP%F{vpnpLIh-@&6+)k= zHcLa8dwi)6LZ3jEM@+S{oB7xDWhY@rfVpK0uDPlL>ITfGH&OK*1%W-qQab zEoS90DWVR zO`uZS&Z?~~f3&CK^Y_vnczzo)gmGeJ4QH%!aHU3@6=hA4q);1j&RGwKDNK1I9>I5v zbj8eyW{u5e$~+o9u02cAe7Lo*}G-T*MMr)d<~6s z5!EU!pFvgod#V;t&GMLoWsjs;WyZ{F#j7Jz>i;#>Jy0!yd}hdZ)We!zjC2Q7TDRPO zQl+X?sg9&7HYCS+E_4JV5BU8)FH(rOoDRyigG4*Xv9r5_6gwM~x!r6s@uZxKh~~s| za!yT-kBtn-sau-KeN}j-gr~d+F!O;n3`$|j4R1#>*`2L*^PFK~q;-AQe&UJ1I`1B~ zMbIEq@Y5jFX6_T$(b4|)x@o~UH8$8l2~!_tBY9q_ic-ocgCCFAJejCI@V zzaXbM#UY53KvoowV}EbUx;!^MI@FImwH-YJhJ{tj))s!wx@mECVgvzHTN=whIWe0P zKid6Z1L(!R?Ait4)N_WtpC3s~kn z9E-zX=<&IGjdZl}82snFZkPSgwzG-D7iJAph@Ch-F=<>hZ`d53U^EURqs(K#a|qZE zTUDRu6tgu4d)roIk(Q>$hX;DwTbmndVI+b$9-qC}EP3_O?gy-n`r)^d?!-bQK`6DV zzFK8{tVZFIdgz1huaVCgiPm_kHF!B6kq}Wth}CLQqzG695@Zg^ZFyqnsYx>58y*_y z@9mURcgXv8`7{adrgC=X^93NIC_xo3O4fgwYp zVN5~diH0NT&i0Pm7mD7-6Ywv+3WGiDJ6jg>k~BHkqp#~5nO?FwE@Ka1q-3l^U-O-q z*Fy3nw+Vn%{$bY7Q`oEob4iE(M2WaG-x?9>P%9Mb2x zSMyo_Soedx5L&c#y+c0+HD+e*WF;kItg?w)9q~MrbHq2&9GgF(P{Dx{I7Gao7wduq zk??^vPiD`M`kM>b!}URFOCI^V!G#azYn;fnQ|tYjOBtbvB?-QwlIAe0Z|qXzitu?sm3rJzNBx~^ z-Nk5GD&(V2di~IO=?un)hGfG7DWXtkay)F`UNsDMHP=@Z7v^i8UR3W~K`8PBH?_S( z5=OGN+0N_3Xp08PF*b`3NGqrf#_7`X16XO}<=km^=o1i0y2;J5{(_S*ute2eUg?^kdG zb!u<*EioU^0(1h`fqTF;par<g)XexC?s z)Helg`7FG?BXAq|i2bzvp0~5|{Tn_jyF=X~aNW1T>uUm6f%N|{`!+eQ2wVoz`IdZJ z92W&H2%Hx<>)U22w3C3;-YC%E+u`*Yfm1*>jS=)qEt8S$JjnW!I*@>E9&yP{g3zM!uHs zWVmnrt{9K-F`_s7i68Mku*AD$N$lciWqh#bM?-COWqD~y;TMWSKp}VDSL9TRoHwYz z5?u(Cv_#>;gxkFPeg077`Znm?UW1YT^!RXpch{)2y6gJg1N=|yz$1UFyZ@(>Y%eO2kvP8iR)D>B>A}O+D zMaYsBAxl<-EHMdLqR0djWP>70%tDr!g)A`(S+Xi*$*PbgicDC63{Yf=A{)%8E3#xw z$dYv-OV))fSr@XzB4mk0$P$Z?B~~Fz6xpx|nLzLCEyw`E9f8|GMV4#`S)#~-4afvV zmMFLm89?~mMA!dI`ko$ zW=&z`Q0F(wqyFPC$HX?3%<4rd%BBukLWOPKoVbIHawGa?&l#zHH>=6}n&uZ)N2K3$ zG07|0NJQnTSv|U$c_zt3y4Wj8QZwyM2SX!~O}&)3P~so-V$aBy30hNYcHj6#FBUqO zZ|KDqe7m~Yr@pZysSqgzaaM>b5?_%;ehK`f(^M~iK~+;L&EOz01B*ApH(f=cVbfk-?rWs4DA_O6FTRBIHr+Md1MQB843g6hsvN r%tBh6R|WC-kbaHyRkJhWBZGZ#n``~i@W(mmlRG{ncckBn?En5BR8q}z literal 0 HcmV?d00001 diff --git a/adaptations/testData/Kawaii_K5000/full bank E midiOX K5000r.syx b/adaptations/testData/Kawaii_K5000/full bank E midiOX K5000r.syx new file mode 100644 index 0000000000000000000000000000000000000000..b71d7672c06a760dc1f902c963ffe74eec8a47ac GIT binary patch literal 108768 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 gze Date: Tue, 25 Feb 2025 01:45:12 +0100 Subject: [PATCH 03/13] Fix name, add test function --- adaptations/KawaiK5000.py | 10 ++++++++++ .../full bank A midiOX K5000r.syx | Bin .../full bank D midiOX K5000r.syx | Bin .../full bank E midiOX K5000r.syx | Bin 4 files changed, 10 insertions(+) rename adaptations/testData/{Kawaii_K5000 => Kawai_K5000}/full bank A midiOX K5000r.syx (100%) rename adaptations/testData/{Kawaii_K5000 => Kawai_K5000}/full bank D midiOX K5000r.syx (100%) rename adaptations/testData/{Kawaii_K5000 => Kawai_K5000}/full bank E midiOX K5000r.syx (100%) diff --git a/adaptations/KawaiK5000.py b/adaptations/KawaiK5000.py index 8cde815b..e8ba5c9a 100644 --- a/adaptations/KawaiK5000.py +++ b/adaptations/KawaiK5000.py @@ -7,6 +7,8 @@ import struct from typing import List, Dict, Any + +import testing from knobkraft.sysex import findSysexDelimiters K5000_SPECIFIC_DEVICE = None @@ -411,3 +413,11 @@ def toneMapToData(include: List[bool]) -> bytes: # not used currently data.append(int(byte_str, 2)) return bytes(data) + + +def make_test_data(): + def bankGenerator(test_data: testing.TestData) -> List[int]: + yield test_data.all_messages + + return testing.TestData(sysex=R"testData/Kawai_K5000/full bank A midiOX K5000r.syx", + bank_generator=bankGenerator) diff --git a/adaptations/testData/Kawaii_K5000/full bank A midiOX K5000r.syx b/adaptations/testData/Kawai_K5000/full bank A midiOX K5000r.syx similarity index 100% rename from adaptations/testData/Kawaii_K5000/full bank A midiOX K5000r.syx rename to adaptations/testData/Kawai_K5000/full bank A midiOX K5000r.syx diff --git a/adaptations/testData/Kawaii_K5000/full bank D midiOX K5000r.syx b/adaptations/testData/Kawai_K5000/full bank D midiOX K5000r.syx similarity index 100% rename from adaptations/testData/Kawaii_K5000/full bank D midiOX K5000r.syx rename to adaptations/testData/Kawai_K5000/full bank D midiOX K5000r.syx diff --git a/adaptations/testData/Kawaii_K5000/full bank E midiOX K5000r.syx b/adaptations/testData/Kawai_K5000/full bank E midiOX K5000r.syx similarity index 100% rename from adaptations/testData/Kawaii_K5000/full bank E midiOX K5000r.syx rename to adaptations/testData/Kawai_K5000/full bank E midiOX K5000r.syx From 2484af965acef8b5d615f2bd0ceac40bb7a936fc Mon Sep 17 00:00:00 2001 From: markusschloesser <59286549+markusschloesser@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:35:21 +0100 Subject: [PATCH 04/13] test data for 1 single sound --- .../Kawai_K5000/single sound bank A patch 1.syx | Bin 0 -> 2940 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 adaptations/testData/Kawai_K5000/single sound bank A patch 1.syx diff --git a/adaptations/testData/Kawai_K5000/single sound bank A patch 1.syx b/adaptations/testData/Kawai_K5000/single sound bank A patch 1.syx new file mode 100644 index 0000000000000000000000000000000000000000..7fc9045a5a8b295d0d2507b95a18971136982339 GIT binary patch literal 2940 zcmeH}*H>am5QnQTpyNfN5ySxKjE-3mG13OqLB>EBOo$1?n$xO?^uIiB`?^)PFOECM zJ-bi)GFun#SJmOy)n)gA?;Kh{1%Ors=BnKQjRyx9)$Er=0qpfDV@fe&3=o-};Zt)F zSh;-2-7F3*t^k;twH|!kI?T*zv;bti-fT7tz*MG!u)~7_z=1O_a3zbL{1~`$7@)w& z(NIW%b|7zCpH;zbs9ZxeCb+;UU@}0S z=H8ztNa<0kZb?<39>%OH@H(jqrwUlcfg7@hsW-V|k`Nfd{VO;FRgDa{%a39D(Q~uM zd4Xg9yvT~L0qj;6aL68}3ZzN7LIG75fLn0)KdYizsRHHYP}rslh+A>VDSso}2@o6F zqO>s98_XqDMJPBIc-8t}R2j~~p|7eY090Cp!j4u~y#nG*T{pD~kW@K@AykI{LTTE% zSvV<*%)FQjGvGo%N30+p@elSQp7D`J-m}PyCw%_x{I~LX@w>PZry?gd-ZuZOujMa$ zf4HyStApDiE6KbKe)z>g{{H6j^eDH#v%Q)4GAY9JnBaf@nsopL0uVB42ZUbQEm;Q- zouY^sq+1dM=b3h>9!f{+Kj+bJ7guLHhes#Jxs!{#=R#51wB+%Ww<#amjU1R`bWLn-*gWh;xUkwJyz)c3povSNxb-kN_elnjACYP6YmzO6? z8`niWWLg`pmZ9HA!@*!MJqCS!b(4M(js|K5h_Q;1+OS_tEDcW7Bge*?>V}acadq0o z{UVr9XNiHNd0`YekW!;=WN*s6=$`V6u5@Lr0ojr6i+QH}`~9b#{UfeP;ZIwl4by(% zOIOY}BAeEFBO}O$wVvt58NYC)yTeV$+H~h=GqO6>5u(_UY#$2E`h_j=sgGz$w00BC z@z&2F?5|k$eHuoVEqmLD#-(O2(GY8F8Ag4GHZ% Date: Thu, 27 Feb 2025 02:01:21 +0100 Subject: [PATCH 05/13] completely new extractPatchesFromAllBankMessages code, works a lot better (old code was plain wrong) but now throws errors when hitting namefromdump --- adaptations/KawaiK5000.py | 144 ++++++++++++++------------------------ 1 file changed, 53 insertions(+), 91 deletions(-) diff --git a/adaptations/KawaiK5000.py b/adaptations/KawaiK5000.py index e8ba5c9a..877ba3a4 100644 --- a/adaptations/KawaiK5000.py +++ b/adaptations/KawaiK5000.py @@ -143,7 +143,7 @@ def isSingleProgramDump(message): # ✅ # Verify Sysex header for a Kawai K5000 program dump return (message[0] == 0xF0 # Start of SysEx and message[1] == KawaiSysexID # Kawai manufacturer ID - and 0x00 <= message[2] <= 0x0F # MIDI channel (0-F for ch 1-16) + and message[3] == OneBlockDump # Function ID for single dump and message[4] == 0x00 # Reserved and message[5] == 0x0A @@ -226,7 +226,7 @@ def isBankDumpFinished(messages): return any(isPartOfBankDump(message) for message in messages) -# https://github.com/coniferprod/k5ktools/blob/main/bank.py + # https://github.com/coniferprod/KSynthLib/blob/master/KSynthLib/K5000/ToneMap.cs#L27 MAX_PATCH_COUNT = 128 TONE_COMMON_DATA_SIZE = 82 @@ -266,114 +266,66 @@ def isBankDumpFinished(messages): } -def parsePatchSizesFromBank(message: bytes) -> Dict[int, Dict[str, Any]]: # ❌❌❌❌❌❌❌❌ - pointer_table = {} - - sysex_header_size = 8 + 19 # SysEx header (8 bytes) + Tone map (19 bytes) - pointer_table_offset = sysex_header_size - - # Each pointer entry has 7 pointers, each 4 bytes (28 bytes per entry) - for patch_index in range(MAX_PATCH_COUNT): - entry_offset = pointer_table_offset + (patch_index * 28) - entry = struct.unpack_from('>7I', message, entry_offset) - - tone_ptr = entry[0] - source_ptrs = entry[1:] - - if tone_ptr != 0: - pointer_table[patch_index] = {'tone': tone_ptr, 'sources': source_ptrs} - - # The high_ptr indicates the end of the patch data area - high_ptr_offset = pointer_table_offset + MAX_PATCH_COUNT * 28 - high_ptr = struct.unpack_from('>I', message, high_ptr_offset)[0] - - if not pointer_table: - raise ValueError("Pointer table is empty, invalid bank message") - - # Base pointer is the smallest tone pointer, used as offset reference - base_ptr = min(entry['tone'] for entry in pointer_table.values()) - - print(f"base_ptr calculated: {base_ptr}") - - for entry in pointer_table.values(): - entry['tone'] -= base_ptr - entry['sources'] = tuple(src - base_ptr if src != 0 else 0 for src in entry['sources']) - - high_ptr -= base_ptr - - # Calculate the correct data region offset for actual patch data - data_region_offset = high_ptr_offset + 4 - patch_data = message[data_region_offset:data_region_offset + POOL_SIZE] - - patch_info = {} - sorted_pointers = sorted(pointer_table.items(), key=lambda item: item[1]['tone']) - print(f"Pointer table has {len(sorted_pointers)} pointers") - for idx, (index, entry) in enumerate(sorted_pointers): - tone_ptr = entry['tone'] - - print(f"Processing patch index {index}: tone_ptr={tone_ptr}") +def extractPatchesFromAllBankMessages(messages): + if not messages: + raise ValueError("No messages received for bank dump.") - if tone_ptr + SOURCE_COUNT_OFFSET >= len(patch_data): - print(f"Index out of range for patch {index}, tone_ptr={tone_ptr}, skipping") + # Flatten all messages into a single data array (excluding SysEx delimiters) + all_data = [] + for message in messages: + if not isPartOfBankDump(message): continue + all_data.extend(message[8:-1]) # Remove SysEx header/footer - source_count = min(patch_data[tone_ptr + SOURCE_COUNT_OFFSET], MAX_SOURCE_COUNT) - add_kit_count = sum(1 for src in entry['sources'] if src != 0) - patch_size = TONE_COMMON_DATA_SIZE + (SOURCE_DATA_SIZE * source_count) + (ADD_KIT_SIZE * add_kit_count) + # Extract tone map from the first 19 bytes + tone_map_data = all_data[:19] + tone_map = getToneMap(tone_map_data) - # Confirm calculated patch size does not exceed data region bounds - if tone_ptr + patch_size > len(patch_data): - continue # Skip invalid-sized patches - - patch_info[index] = { - 'offset': tone_ptr, - 'size': patch_size, - 'source_count': source_count, - 'add_kit_count': add_kit_count - } - - print(f"Patch {index} info: offset={tone_ptr}, size={patch_size}, source_count={source_count}, add_kit_count={add_kit_count}") - - return patch_info + patch_count = sum(tone_map) # Number of patches present in the dump + print(f"Contains {patch_count} patches.") + # Remaining patch data (skip tone map and padding) + patch_data_start = 19 + patch_data = all_data[patch_data_start:] -def extractPatchesFromAllBankMessages(messages, channel=None): # ❌❌❌❌❌❌ does NOT work, problem either here or in parsePatchSizesFromBank, have tried at least 50 times + offset = 0 patches = [] - message = messages[0] if isinstance(messages[0], list) else messages - tone_map_data = message[8:27] - tone_map = getToneMap(bytes(tone_map_data)) - - # Pass the correct slice including pointer table and data - bank_data = bytes(message[27:]) # Correct slice without re-adding header offset - bank_patch_info = parsePatchSizesFromBank(bank_data) - - # Properly aligned data extraction without adding extra offset - data_region_offset = (MAX_PATCH_COUNT * 7 * 4) + 4 - data = bank_data[data_region_offset:-1] + for _ in range(patch_count): + if offset >= len(patch_data): + break - for patch_index, included in enumerate(tone_map): - if not included or patch_index not in bank_patch_info: - continue + # Extract checksum (first byte of patch) + checksum = patch_data[offset] + offset += 1 + print(f"Checksum = {checksum:02X}") - info = bank_patch_info[patch_index] - offset = info['offset'] - patch_size = info['size'] + # Try to determine patch size based on `SINGLE_INFO` + patch_size = None + for size, (pcm, add) in SINGLE_INFO.items(): + if len(patch_data) - offset >= size: + patch_size = size + break - if offset + patch_size > len(data): + if patch_size is None: + print("Unknown patch size, skipping.") continue - patch_data = data[offset: offset + patch_size] - patch_sysex = [0xF0, 0x40, channel, 0x20, 0x00, 0x0A, 0x00, patch_index] + list(patch_data) + [0xF7] - patches.append(patch_sysex) + # Extract patch bytes as a list of integers + current_patch = list(patch_data[offset:offset + patch_size]) - return patches + if len(current_patch) != patch_size: + print(f"Warning: Expected {patch_size} bytes, but got {len(current_patch)}.") + # Append the patch as a list of integers (not bytes) + patches.append(current_patch) + offset += patch_size # Move to the next patch + return patches # Return a list of lists (KnobKraft format) -def getToneMap(data: bytes) -> List[bool]: # ✅ +def getToneMap(data: bytes) -> List[bool]: # ✅ !!!PCM bank has no tone map TONE_COUNT = 128 DATA_SIZE = 19 if len(data) != DATA_SIZE: @@ -385,6 +337,11 @@ def getToneMap(data: bytes) -> List[bool]: # ✅ + + + + + def toneMapToString(include: List[bool]) -> str: # not used currently return ' '.join(str(i + 1) for i, included in enumerate(include) if included) @@ -421,3 +378,8 @@ def bankGenerator(test_data: testing.TestData) -> List[int]: return testing.TestData(sysex=R"testData/Kawai_K5000/full bank A midiOX K5000r.syx", bank_generator=bankGenerator) + + def programs(data: testing.TestData) -> List[testing.ProgramTestData]: + yield testing.ProgramTestData(message=data.all_messages[0], number=1) + + return testing.TestData(sysex=R"testData/Kawai_K5000/single sound bank A patch 1.syx", program_generator=programs) From 4ca7a5b4ddb7fbf02eeb8407029a2d29d63b6ac7 Mon Sep 17 00:00:00 2001 From: markusschloesser <59286549+markusschloesser@users.noreply.github.com> Date: Thu, 27 Feb 2025 18:07:27 +0100 Subject: [PATCH 06/13] first patch extract kind of works, but patch is too many bytes, that's why subsequent one fail --- adaptations/KawaiK5000.py | 75 ++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/adaptations/KawaiK5000.py b/adaptations/KawaiK5000.py index 877ba3a4..474e1753 100644 --- a/adaptations/KawaiK5000.py +++ b/adaptations/KawaiK5000.py @@ -5,11 +5,9 @@ # https://github.com/coniferprod/KSynthLib/blob/master/KSynthLib/K5000/SystemExclusive.cs -import struct -from typing import List, Dict, Any +from typing import List, Dict import testing -from knobkraft.sysex import findSysexDelimiters K5000_SPECIFIC_DEVICE = None @@ -270,6 +268,8 @@ def extractPatchesFromAllBankMessages(messages): if not messages: raise ValueError("No messages received for bank dump.") + bank_byte = messages[0][7] if len(messages[0]) > 7 else 0x00 # Ensure it's a valid integer + # Flatten all messages into a single data array (excluding SysEx delimiters) all_data = [] for message in messages: @@ -281,7 +281,10 @@ def extractPatchesFromAllBankMessages(messages): tone_map_data = all_data[:19] tone_map = getToneMap(tone_map_data) - patch_count = sum(tone_map) # Number of patches present in the dump + # Extract available patch numbers from the tone map + patch_numbers = [i for i, present in enumerate(tone_map) if present] + + patch_count = len(patch_numbers) # Number of patches present in the dump print(f"Contains {patch_count} patches.") # Remaining patch data (skip tone map and padding) @@ -291,40 +294,62 @@ def extractPatchesFromAllBankMessages(messages): offset = 0 patches = [] - for _ in range(patch_count): + for i, patch_number in enumerate(patch_numbers): if offset >= len(patch_data): + print(f"Warning: Reached end of patch data unexpectedly at patch {i}.") break # Extract checksum (first byte of patch) - checksum = patch_data[offset] - offset += 1 - print(f"Checksum = {checksum:02X}") - - # Try to determine patch size based on `SINGLE_INFO` - patch_size = None - for size, (pcm, add) in SINGLE_INFO.items(): - if len(patch_data) - offset >= size: - patch_size = size - break - - if patch_size is None: - print("Unknown patch size, skipping.") - continue + checksum = patch_data[offset] # Extract checksum at current offset + print(f"Checksum for patch {i+1} (Patch Number {patch_number}) = {checksum:02X}") + + offset += 1 # Move past checksum + + # Determine patch size based on `SINGLE_INFO` + remaining_bytes = len(patch_data) - offset + valid_sizes = [size for size in SINGLE_INFO.keys() if size <= remaining_bytes] + + if not valid_sizes: + print(f"Error: No valid patch sizes found for remaining {remaining_bytes} bytes at patch {i+1}. Skipping.") + break # Prevent infinite errors + + # Find the closest matching patch size + patch_size = min(valid_sizes, key=lambda s: abs(remaining_bytes - s)) - # Extract patch bytes as a list of integers - current_patch = list(patch_data[offset:offset + patch_size]) + # Extract patch data + current_patch = patch_data[offset:offset + patch_size] + # Ensure extracted data length is correct if len(current_patch) != patch_size: - print(f"Warning: Expected {patch_size} bytes, but got {len(current_patch)}.") + print(f"Error: Extracted {len(current_patch)} bytes, expected {patch_size}. Skipping patch {i+1}.") + continue + + # Restore SysEx header/footer and insert the patch number + formatted_patch = [ + 0xF0, KawaiSysexID, 0x00, OneBlockDump, 0x00, 0x0A, 0x00, bank_byte, patch_number, checksum + ] + list(current_patch) + [0xF7] - # Append the patch as a list of integers (not bytes) - patches.append(current_patch) + # Debug: Print first bytes of patch + print(f"Patch {i+1} first bytes: {' '.join(f'{byte:02X}' for byte in formatted_patch[:20])}") + + # Validate patch + if not isSingleProgramDump(formatted_patch): + print(f"Error: Extracted patch {i+1} is NOT a valid program dump. Skipping.") + continue - offset += patch_size # Move to the next patch + # Append the valid patch + patches.append(formatted_patch) + # Move offset **AFTER** extracting the patch + print(f"Moving offset from {offset} to {offset + patch_size}") + offset += patch_size + + print(f"Extracted {len(patches)} valid patches.") return patches # Return a list of lists (KnobKraft format) + + def getToneMap(data: bytes) -> List[bool]: # ✅ !!!PCM bank has no tone map TONE_COUNT = 128 DATA_SIZE = 19 From fbd5ee667d923bdb1cbf321a9d8812ddfda254ea Mon Sep 17 00:00:00 2001 From: markusschloesser <59286549+markusschloesser@users.noreply.github.com> Date: Fri, 28 Feb 2025 01:14:45 +0100 Subject: [PATCH 07/13] source count calculation included but not working correctly, therefor offsets also wrong --- adaptations/KawaiK5000.py | 110 ++++++++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 17 deletions(-) diff --git a/adaptations/KawaiK5000.py b/adaptations/KawaiK5000.py index 474e1753..8c8306cc 100644 --- a/adaptations/KawaiK5000.py +++ b/adaptations/KawaiK5000.py @@ -300,30 +300,63 @@ def extractPatchesFromAllBankMessages(messages): break # Extract checksum (first byte of patch) - checksum = patch_data[offset] # Extract checksum at current offset + checksum = patch_data[offset] + offset += 1 print(f"Checksum for patch {i+1} (Patch Number {patch_number}) = {checksum:02X}") - offset += 1 # Move past checksum + # Remaining bytes to process + bytes_left = len(patch_data) - offset - # Determine patch size based on `SINGLE_INFO` - remaining_bytes = len(patch_data) - offset - valid_sizes = [size for size in SINGLE_INFO.keys() if size <= remaining_bytes] + # Extract patch data dynamically + patch_body = patch_data[offset:offset + bytes_left] + print(f"Patch data is from {offset} to {offset + bytes_left} ({len(patch_body)} bytes)") - if not valid_sizes: - print(f"Error: No valid patch sizes found for remaining {remaining_bytes} bytes at patch {i+1}. Skipping.") - break # Prevent infinite errors + if len(patch_body) < 60: # Ensure it's large enough to contain source info + print(f"Error: Patch {i+1} is too small to be valid. Skipping.") + continue + + # **Print the first 20 bytes of patch data for debugging** + print(f"Patch {i+1} raw first 20 bytes: {' '.join(f'{b:02X}' for b in patch_body[:20])}") - # Find the closest matching patch size - patch_size = min(valid_sizes, key=lambda s: abs(remaining_bytes - s)) + # **Improved Source Count Extraction** + detected_source_offset = None + for possible_offset in range(40, 80): # Expand the search range + if patch_body[possible_offset] <= 6: # Valid source count should be 0-6 + detected_source_offset = possible_offset + break - # Extract patch data - current_patch = patch_data[offset:offset + patch_size] + if detected_source_offset is None: + print(f"Error: Could not detect a valid source count offset for patch {i+1}. Skipping.") + continue - # Ensure extracted data length is correct - if len(current_patch) != patch_size: - print(f"Error: Extracted {len(current_patch)} bytes, expected {patch_size}. Skipping patch {i+1}.") + source_count = patch_body[detected_source_offset] # Extract source count + source_count = min(source_count, 6) # Max sources should be 6 + print(f"Patch {i+1} detected {source_count} sources at offset {detected_source_offset}.") + + # **Check if it's PCM or ADD sources** + is_additive = False # Default to PCM synthesis + + if source_count > 0: + # Ensure offset is valid before reading wave data + if detected_source_offset + source_count < len(patch_body): + wave_data = patch_body[detected_source_offset + 1: detected_source_offset + 1 + source_count] + is_additive = any(wave_data) # If any wave data is non-zero, it's additive + + # **Determine correct patch size** + if is_additive: + patch_size = TONE_COMMON_DATA_SIZE + (source_count * ADD_KIT_SIZE) + print(f"Patch {i+1} is ADDITIVE with {source_count} sources. Calculated size: {patch_size}") + else: + patch_size = TONE_COMMON_DATA_SIZE + (source_count * SOURCE_DATA_SIZE) + print(f"Patch {i+1} is PCM with {source_count} sources. Calculated size: {patch_size}") + + if patch_size <= 0 or patch_size > bytes_left: + print(f"Error: Invalid patch size detected for patch {i+1}. Skipping.") continue + # Extract only the correct patch size + current_patch = patch_body[:patch_size] + # Restore SysEx header/footer and insert the patch number formatted_patch = [ 0xF0, KawaiSysexID, 0x00, OneBlockDump, 0x00, 0x0A, 0x00, bank_byte, patch_number, checksum @@ -340,9 +373,9 @@ def extractPatchesFromAllBankMessages(messages): # Append the valid patch patches.append(formatted_patch) - # Move offset **AFTER** extracting the patch - print(f"Moving offset from {offset} to {offset + patch_size}") + # Move offset forward correctly offset += patch_size + print(f"Moving offset to {offset}") print(f"Extracted {len(patches)} valid patches.") return patches # Return a list of lists (KnobKraft format) @@ -350,6 +383,49 @@ def extractPatchesFromAllBankMessages(messages): +def determinePatchSize(patch_data): + """ + Determines the patch size dynamically by analyzing the structure. + - Extracts the number of PCM and ADD sources. + - Calculates the correct patch size. + + Returns (pcm_count, add_count, patch_size) + """ + if len(patch_data) < 10: + return 0, 0, 0 # Invalid patch + + # Extract the number of sources + pcm_count = 0 + add_count = 0 + patch_size = 0 + + # Extract source count offset (based on C# logic) + source_count_offset = 51 + if len(patch_data) > source_count_offset: + source_count = patch_data[source_count_offset] + print(f"Detected {source_count} sources in patch.") + + # Determine PCM vs ADD sources + for i in range(source_count): + source_type_offset = source_count_offset + 1 + (i * 86) # Each source is 86 bytes + if len(patch_data) > source_type_offset: + is_additive = patch_data[source_type_offset] # Check if ADD type + if is_additive: + add_count += 1 + else: + pcm_count += 1 + + # Compute patch size based on extracted data + if add_count > 0: + patch_size = TONE_COMMON_DATA_SIZE + (add_count * ADD_KIT_SIZE) + else: + patch_size = TONE_COMMON_DATA_SIZE + (pcm_count * SOURCE_DATA_SIZE) + + return pcm_count, add_count, patch_size + + + + def getToneMap(data: bytes) -> List[bool]: # ✅ !!!PCM bank has no tone map TONE_COUNT = 128 DATA_SIZE = 19 From 814aabac75ec4d38519342cb9db1e9f413ab76db Mon Sep 17 00:00:00 2001 From: markusschloesser <59286549+markusschloesser@users.noreply.github.com> Date: Sun, 2 Mar 2025 01:26:01 +0100 Subject: [PATCH 08/13] =?UTF-8?q?extractPatchesFromAllBankMessages=20works?= =?UTF-8?q?=20=E2=9C=85=F0=9F=92=AA,=20tested=20with=20lots=20of=20differe?= =?UTF-8?q?nt=20banks.=20Still=20lots=20of=20debug=20print=20in=20there.?= =?UTF-8?q?=20And=20kk=20crashes=20when=20clicking=20on=20a=20patch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adaptations/KawaiK5000.py | 136 ++++++++++++++++---------------------- 1 file changed, 57 insertions(+), 79 deletions(-) diff --git a/adaptations/KawaiK5000.py b/adaptations/KawaiK5000.py index 8c8306cc..5548ccc6 100644 --- a/adaptations/KawaiK5000.py +++ b/adaptations/KawaiK5000.py @@ -301,8 +301,8 @@ def extractPatchesFromAllBankMessages(messages): # Extract checksum (first byte of patch) checksum = patch_data[offset] - offset += 1 print(f"Checksum for patch {i+1} (Patch Number {patch_number}) = {checksum:02X}") + offset += 1 # ✅ Move past checksum # Remaining bytes to process bytes_left = len(patch_data) - offset @@ -315,54 +315,73 @@ def extractPatchesFromAllBankMessages(messages): print(f"Error: Patch {i+1} is too small to be valid. Skipping.") continue - # **Print the first 20 bytes of patch data for debugging** - print(f"Patch {i+1} raw first 20 bytes: {' '.join(f'{b:02X}' for b in patch_body[:20])}") + # Extract source count (from Common Data byte 50) + source_count_offset = 50 + source_count = min(patch_body[source_count_offset], 6) # Max 6 sources + print(f"Patch {i + 1} detected {source_count} sources.") - # **Improved Source Count Extraction** - detected_source_offset = None - for possible_offset in range(40, 80): # Expand the search range - if patch_body[possible_offset] <= 6: # Valid source count should be 0-6 - detected_source_offset = possible_offset + # ---- PCM/ADD Classification Per Source ---- + add_count = 0 + pcm_count = 0 + source_type_offset = TONE_COMMON_DATA_SIZE + 27 # First wave type + + for s in range(source_count): + source_offset = source_type_offset + (s * SOURCE_DATA_SIZE) + + if source_offset + 1 >= len(patch_body): # Avoid out-of-bounds access + print(f"Patch {i+1} Source {s+1}: Warning - Not enough data for wave type, assuming PCM.") + pcm_count += 1 + continue + + # Extract MSB and LSB + wave_msb = patch_body[source_offset] + wave_lsb = patch_body[source_offset + 1] + wave_type = ((wave_msb & 0b111) << 7) | wave_lsb # Correctly extract wave type + + is_add = (wave_type == 512) # ✅ Only wave_type 512 is ADD + + if is_add: + add_count += 1 + else: + pcm_count += 1 + + print(f"Patch {i+1} Source {s+1}: Wave MSB: {wave_msb:02X}, LSB: {wave_lsb:02X} -> Type: {wave_type} -> {'ADD' if is_add else 'PCM'}") + + # Debug: Total ADD vs PCM count + print(f"Patch {i + 1}: {add_count} ADD, {pcm_count} PCM") + + # ---- Get Patch Size from SINGLE_INFO ---- + patch_size = None + for size, (pcm, add) in SINGLE_INFO.items(): + if pcm == pcm_count and add == add_count: + patch_size = size break - if detected_source_offset is None: - print(f"Error: Could not detect a valid source count offset for patch {i+1}. Skipping.") + if patch_size is None: + print( + f"Error: Could not determine patch size for {pcm_count} PCM, {add_count} ADD sources in Patch {i + 1}. Skipping.") continue - source_count = patch_body[detected_source_offset] # Extract source count - source_count = min(source_count, 6) # Max sources should be 6 - print(f"Patch {i+1} detected {source_count} sources at offset {detected_source_offset}.") - - # **Check if it's PCM or ADD sources** - is_additive = False # Default to PCM synthesis - - if source_count > 0: - # Ensure offset is valid before reading wave data - if detected_source_offset + source_count < len(patch_body): - wave_data = patch_body[detected_source_offset + 1: detected_source_offset + 1 + source_count] - is_additive = any(wave_data) # If any wave data is non-zero, it's additive - - # **Determine correct patch size** - if is_additive: - patch_size = TONE_COMMON_DATA_SIZE + (source_count * ADD_KIT_SIZE) - print(f"Patch {i+1} is ADDITIVE with {source_count} sources. Calculated size: {patch_size}") - else: - patch_size = TONE_COMMON_DATA_SIZE + (source_count * SOURCE_DATA_SIZE) - print(f"Patch {i+1} is PCM with {source_count} sources. Calculated size: {patch_size}") - - if patch_size <= 0 or patch_size > bytes_left: - print(f"Error: Invalid patch size detected for patch {i+1}. Skipping.") + print(f"Patch {i + 1} calculated size from SINGLE_INFO: {patch_size}") + + if patch_size <= 0 or patch_size > bytes_left + 1: + print( + f"Error: Invalid patch size detected for patch {i + 1}. Skipping. Expected {patch_size}, but only {bytes_left} remain.") continue - # Extract only the correct patch size - current_patch = patch_body[:patch_size] + # ---- Extract only the correct patch size ---- + current_patch = patch_body[:patch_size - 1] # ✅ Exclude last byte (extra checksum) # Restore SysEx header/footer and insert the patch number formatted_patch = [ - 0xF0, KawaiSysexID, 0x00, OneBlockDump, 0x00, 0x0A, 0x00, bank_byte, patch_number, checksum + 0xF0, KawaiSysexID, 0x00, OneBlockDump, 0x00, 0x0A, 0x00, bank_byte, patch_number, int(checksum) ] + list(current_patch) + [0xF7] - # Debug: Print first bytes of patch + # Debug: Print full SysEx for the first patch + if i == 0: # Only for the first patch + full_sysex_hex = ' '.join(f'{byte:02X}' for byte in formatted_patch) + print(f"Full SysEx for Patch 1:\n{full_sysex_hex}") + print(f"Patch {i+1} first bytes: {' '.join(f'{byte:02X}' for byte in formatted_patch[:20])}") # Validate patch @@ -374,7 +393,7 @@ def extractPatchesFromAllBankMessages(messages): patches.append(formatted_patch) # Move offset forward correctly - offset += patch_size + offset += patch_size - 1 # ✅ Ensure correct alignment for next patch print(f"Moving offset to {offset}") print(f"Extracted {len(patches)} valid patches.") @@ -383,47 +402,6 @@ def extractPatchesFromAllBankMessages(messages): -def determinePatchSize(patch_data): - """ - Determines the patch size dynamically by analyzing the structure. - - Extracts the number of PCM and ADD sources. - - Calculates the correct patch size. - - Returns (pcm_count, add_count, patch_size) - """ - if len(patch_data) < 10: - return 0, 0, 0 # Invalid patch - - # Extract the number of sources - pcm_count = 0 - add_count = 0 - patch_size = 0 - - # Extract source count offset (based on C# logic) - source_count_offset = 51 - if len(patch_data) > source_count_offset: - source_count = patch_data[source_count_offset] - print(f"Detected {source_count} sources in patch.") - - # Determine PCM vs ADD sources - for i in range(source_count): - source_type_offset = source_count_offset + 1 + (i * 86) # Each source is 86 bytes - if len(patch_data) > source_type_offset: - is_additive = patch_data[source_type_offset] # Check if ADD type - if is_additive: - add_count += 1 - else: - pcm_count += 1 - - # Compute patch size based on extracted data - if add_count > 0: - patch_size = TONE_COMMON_DATA_SIZE + (add_count * ADD_KIT_SIZE) - else: - patch_size = TONE_COMMON_DATA_SIZE + (pcm_count * SOURCE_DATA_SIZE) - - return pcm_count, add_count, patch_size - - def getToneMap(data: bytes) -> List[bool]: # ✅ !!!PCM bank has no tone map From 53ac8d2d248a2782458077d48a5105f2800a1cf4 Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 2 Mar 2025 14:00:01 +0100 Subject: [PATCH 09/13] Fix crash happening when a program buffer synth did return an empty list of bank descriptors. --- MidiKraft | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MidiKraft b/MidiKraft index 7446d41e..cb3cecdc 160000 --- a/MidiKraft +++ b/MidiKraft @@ -1 +1 @@ -Subproject commit 7446d41e803c83f4c8816bd68852e27f706c4014 +Subproject commit cb3cecdc10b9f3592ed1c37dfce907f0508e42b9 From e79e6e40b0c1ffdda3f249ef5a8beec3cb5eee1d Mon Sep 17 00:00:00 2001 From: markusschloesser <59286549+markusschloesser@users.noreply.github.com> Date: Sun, 2 Mar 2025 18:04:43 +0100 Subject: [PATCH 10/13] debug and code clean up, documentation added. convertToProgramDump fixed. consistent usage of bankdescriptors --- adaptations/KawaiK5000.py | 167 +++++++++++++++++++------------------- 1 file changed, 85 insertions(+), 82 deletions(-) diff --git a/adaptations/KawaiK5000.py b/adaptations/KawaiK5000.py index 5548ccc6..a6a9f55c 100644 --- a/adaptations/KawaiK5000.py +++ b/adaptations/KawaiK5000.py @@ -2,8 +2,12 @@ # S and R have Bank D # If expansion installed also Banks E and F # W additionally for the Rom parts Bank B - +# lots of inspiration from: # https://github.com/coniferprod/KSynthLib/blob/master/KSynthLib/K5000/SystemExclusive.cs +# and https://github.com/coniferprod/KSynthLib/blob/master/Driver/Program.cs#L137 + +# Adaptation written by Markus Schlösser + from typing import List, Dict @@ -71,27 +75,37 @@ def needsChannelSpecificDetection(): # ✅ def bankDescriptors() -> List[Dict]: + """Returns a list of available banks based on the detected K5000 model.""" global K5000_SPECIFIC_DEVICE if K5000_SPECIFIC_DEVICE is None: return [] # Prevent errors if called before detection - base_banks = [(0x00, "A", 100)] # "100" needs to be changed back to 128 once kk can do skipping + # Base bank configurations + base_banks = [ + {"id": 0x00, "name": "A", "size": 100} # Adjust to 128 once skipping is implemented + ] if K5000_SPECIFIC_DEVICE == "K5000W": - base_banks.append((0x01, "B", 128)) # ROMpler Bank for W + base_banks.append({"id": 0x01, "name": "B", "size": 128}) # ROMpler Bank for W if K5000_SPECIFIC_DEVICE in ["K5000S", "K5000R"]: - base_banks.append((0x02, "D", 40)) # needs to be changed back to 128 once kk can do skipping - base_banks.extend([(0x03, "E", 51), (0x04, "F", 128)]) # Expansion banks # needs to be changed back to 128 once kk can do skipping - - return [{ - "bank": b[0], - "name": f"Bank {b[1]}", - "size": b[2], - "type": "Patch", - "isROM": (b[1] == "B") # Only ROM for K5000W Bank B - } for b in base_banks] + base_banks.append({"id": 0x02, "name": "D", "size": 40}) # Adjust to 128 once skipping is implemented + base_banks.extend([ + {"id": 0x03, "name": "E", "size": 51}, + {"id": 0x04, "name": "F", "size": 128} + ]) # Expansion banks + + return [ + { + "bank": bank["id"], + "name": f"Bank {bank['name']}", + "size": bank["size"], + "type": "Patch", + "isROM": bank.get("isROM", False) + } + for bank in base_banks + ] def createEditBufferRequest(channel): @@ -99,22 +113,22 @@ def createEditBufferRequest(channel): return [] -def createProgramDumpRequest(channel, patchNo): # ✅ - global K5000_SPECIFIC_DEVICE +def createProgramDumpRequest(channel: int, patchNo: int) -> List[int]: + """ + Creates a SysEx message to request a program dump for a given patch number. + + Parameters: + - channel (int): MIDI channel (0-15) + - patchNo (int): Patch number (0-based index across all banks) + Returns: + - List[int]: SysEx message bytes + """ banks = bankDescriptors() - total_patches = 0 - selected_bank = None patch_number = patchNo + selected_bank = None - # Correct mapping of bank indexes to SysEx bank bytes - bank_byte_map = { - "A": 0x00, - "D": 0x02, - "E": 0x03, - "F": 0x04 - } - + # Identify the correct bank and adjust the patch number accordingly for bank in banks: if patch_number < bank["size"]: selected_bank = bank @@ -124,13 +138,13 @@ def createProgramDumpRequest(channel, patchNo): # ✅ if selected_bank is None: raise ValueError(f"Invalid patch number {patchNo}. Exceeds total patch count.") - bank_name = selected_bank["name"].split()[-1] # Extract the letter (A, D, E, F) - bank_byte = bank_byte_map.get(bank_name, 0x00) # Default to A if something goes wrong + # Extract the correct SysEx bank byte + bank_byte = selected_bank["bank"] - sysex_message = [ + # Construct SysEx message + return [ 0xF0, KawaiSysexID, channel, OneBlockDumpRequest, 0x00, 0x0A, 0x00, bank_byte, patch_number, 0xF7 ] - return sysex_message def isSingleProgramDump(message): # ✅ @@ -141,30 +155,55 @@ def isSingleProgramDump(message): # ✅ # Verify Sysex header for a Kawai K5000 program dump return (message[0] == 0xF0 # Start of SysEx and message[1] == KawaiSysexID # Kawai manufacturer ID - and message[3] == OneBlockDump # Function ID for single dump and message[4] == 0x00 # Reserved and message[5] == 0x0A and message[-1] == 0xF7) # End of SysEx -def convertToProgramDump(channel, message, program_number): # ❌❌ K5000 replies with [f0 40 00 41 00 0a f7] when sending from kk, which is "write error" +def convertToProgramDump(channel: int, message: List[int], program_number: int) -> List[int]: + """ + Converts a received program dump into a properly formatted SysEx message, + ensuring the correct bank and patch number assignment based on `bankDescriptors`. + + Parameters: + - channel (int): MIDI channel (0-15) + - message (List[int]): Incoming SysEx message bytes. + - program_number (int): Global patch number across all banks. + + Returns: + - List[int]: Modified SysEx message with updated bank and patch number. + + Raises: + - Exception: If the message is not a valid single program dump. + - ValueError: If the calculated bank number is out of range. + """ if not isSingleProgramDump(message): raise Exception("Invalid message format - can't be converted") - bank_number = program_number // 128 # Determine bank index - patch_number = program_number % 128 # Determine patch within the bank + # Get dynamic bank information from bankDescriptors() + banks = bankDescriptors() - # Determine correct bank byte based on the bank index - bank_byte_map = {0: 0x00, 1: 0x02, 2: 0x03, 3: 0x04} + # Determine correct bank and patch number + patch_number = program_number + selected_bank = None - if bank_number not in bank_byte_map: - raise ValueError(f"Invalid bank number {bank_number}. Must be 0-3.") + for bank in banks: + if patch_number < bank["size"]: + selected_bank = bank + break + patch_number -= bank["size"] + + if selected_bank is None: + raise ValueError(f"Invalid program number {program_number}. Exceeds available patches.") - bank_byte = bank_byte_map[bank_number] + # Extract bank byte dynamically + bank_byte = selected_bank["bank"] - # Reconstruct SysEx message with updated bank and program number - return message[:7] + [bank_byte, patch_number] + message[8:] + # Construct the modified SysEx message + modified_message = message[:7] + [bank_byte, patch_number] + message[9:] + + return modified_message def numberFromDump(message) -> int: # where can I see if successful? @@ -224,7 +263,6 @@ def isBankDumpFinished(messages): return any(isPartOfBankDump(message) for message in messages) - # https://github.com/coniferprod/KSynthLib/blob/master/KSynthLib/K5000/ToneMap.cs#L27 MAX_PATCH_COUNT = 128 TONE_COMMON_DATA_SIZE = 82 @@ -309,7 +347,7 @@ def extractPatchesFromAllBankMessages(messages): # Extract patch data dynamically patch_body = patch_data[offset:offset + bytes_left] - print(f"Patch data is from {offset} to {offset + bytes_left} ({len(patch_body)} bytes)") + # print(f"Patch data is from {offset} to {offset + bytes_left} ({len(patch_body)} bytes)") if len(patch_body) < 60: # Ensure it's large enough to contain source info print(f"Error: Patch {i+1} is too small to be valid. Skipping.") @@ -378,9 +416,9 @@ def extractPatchesFromAllBankMessages(messages): ] + list(current_patch) + [0xF7] # Debug: Print full SysEx for the first patch - if i == 0: # Only for the first patch - full_sysex_hex = ' '.join(f'{byte:02X}' for byte in formatted_patch) - print(f"Full SysEx for Patch 1:\n{full_sysex_hex}") + # if i == 0: # Only for the first patch + # full_sysex_hex = ' '.join(f'{byte:02X}' for byte in formatted_patch) + # print(f"Full SysEx for Patch 1:\n{full_sysex_hex}") print(f"Patch {i+1} first bytes: {' '.join(f'{byte:02X}' for byte in formatted_patch[:20])}") @@ -394,17 +432,13 @@ def extractPatchesFromAllBankMessages(messages): # Move offset forward correctly offset += patch_size - 1 # ✅ Ensure correct alignment for next patch - print(f"Moving offset to {offset}") + # print(f"Moving offset to {offset}") print(f"Extracted {len(patches)} valid patches.") return patches # Return a list of lists (KnobKraft format) - - - - -def getToneMap(data: bytes) -> List[bool]: # ✅ !!!PCM bank has no tone map +def getToneMap(data: bytes) -> List[bool]: # ✅ !!!PCM bank has no tone map, don't yet, what to do with it TONE_COUNT = 128 DATA_SIZE = 19 if len(data) != DATA_SIZE: @@ -420,37 +454,6 @@ def getToneMap(data: bytes) -> List[bool]: # ✅ !!!PCM bank has no tone map - -def toneMapToString(include: List[bool]) -> str: # not used currently - return ' '.join(str(i + 1) for i, included in enumerate(include) if included) - - -def toneMapCount(include: List[bool]) -> int: - print(sum(include)) - return sum(include) - - -def toneMapEquals(map1: List[bool], map2: List[bool]) -> bool: # not used currently - return map1 == map2 - - -def toneMapToData(include: List[bool]) -> bytes: # not used currently - bit_string = '' - for i in range(len(include)): - bit_string += '1' if include[i] else '0' - if (i + 1) % 7 == 0: - bit_string += '0' - - bit_string = bit_string[::-1] # reverse string - - data = [] - for i in range(0, len(bit_string), 8): - byte_str = bit_string[i:i + 8] - data.append(int(byte_str, 2)) - - return bytes(data) - - def make_test_data(): def bankGenerator(test_data: testing.TestData) -> List[int]: yield test_data.all_messages From 727a6231b36b9580349c94629c0b91f680a06420 Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 2 Mar 2025 18:47:12 +0100 Subject: [PATCH 11/13] Adding the program generator to get more test coverage for the K5000 adaptation --- adaptations/KawaiK5000.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/adaptations/KawaiK5000.py b/adaptations/KawaiK5000.py index a6a9f55c..91515f6b 100644 --- a/adaptations/KawaiK5000.py +++ b/adaptations/KawaiK5000.py @@ -448,20 +448,20 @@ def getToneMap(data: bytes) -> List[bool]: # ✅ !!!PCM bank has no tone map return [bit == '1' for bit in bit_string[:TONE_COUNT]] - - - - - - def make_test_data(): + global K5000_SPECIFIC_DEVICE + def bankGenerator(test_data: testing.TestData) -> List[int]: yield test_data.all_messages - return testing.TestData(sysex=R"testData/Kawai_K5000/full bank A midiOX K5000r.syx", - bank_generator=bankGenerator) - def programs(data: testing.TestData) -> List[testing.ProgramTestData]: - yield testing.ProgramTestData(message=data.all_messages[0], number=1) + program_buffers = extractPatchesFromAllBankMessages(data.all_messages) + yield testing.ProgramTestData(program_buffers[0], number=0, name="PowerK5K") + yield testing.ProgramTestData(program_buffers[1], number=1, name="PowerBas") + yield testing.ProgramTestData(program_buffers[-1], number=97, name="Boreal") - return testing.TestData(sysex=R"testData/Kawai_K5000/single sound bank A patch 1.syx", program_generator=programs) + K5000_SPECIFIC_DEVICE = 0x01 + return testing.TestData(sysex=R"testData/Kawai_K5000/full bank A midiOX K5000r.syx", + bank_generator=bankGenerator, + program_generator=programs, + device_detect_call=[0xF0, KawaiSysexID, 0, 0x60, 0xF7]) From 498bcc0fe6cd2d6b70e46fb9f216313c001113ad Mon Sep 17 00:00:00 2001 From: markusschloesser <59286549+markusschloesser@users.noreply.github.com> Date: Tue, 4 Mar 2025 00:27:40 +0100 Subject: [PATCH 12/13] now with calculateFingerprint --- adaptations/KawaiK5000.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/adaptations/KawaiK5000.py b/adaptations/KawaiK5000.py index a6a9f55c..76174c15 100644 --- a/adaptations/KawaiK5000.py +++ b/adaptations/KawaiK5000.py @@ -8,10 +8,10 @@ # Adaptation written by Markus Schlösser - from typing import List, Dict - +from copy import copy import testing +import hashlib K5000_SPECIFIC_DEVICE = None @@ -448,7 +448,15 @@ def getToneMap(data: bytes) -> List[bool]: # ✅ !!!PCM bank has no tone map return [bit == '1' for bit in bit_string[:TONE_COUNT]] +def calculateFingerprint(message: List[int]): + if isSingleProgramDump(message): + patch_name_start = 49 # Adjusted offset to skip the leading zero + patch_name_length = 8 # Names are exactly 8 characters long + patch_name = message[patch_name_start:patch_name_start + patch_name_length] + blanked_out = message[10:-1] + patch_name + return hashlib.md5(bytearray(blanked_out)).hexdigest() # Calculate the fingerprint from the cleaned payload data + raise Exception("Can only fingerprint Presets") From 5a85cc3e8a426e5050f80ed32d2c59ffcfd598fe Mon Sep 17 00:00:00 2001 From: markusschloesser <59286549+markusschloesser@users.noreply.github.com> Date: Tue, 4 Mar 2025 00:54:49 +0100 Subject: [PATCH 13/13] now with calculateFingerprint, fixed Bank ID for bank download --- adaptations/KawaiK5000.py | 50 +++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/adaptations/KawaiK5000.py b/adaptations/KawaiK5000.py index 15129036..15a4fb3b 100644 --- a/adaptations/KawaiK5000.py +++ b/adaptations/KawaiK5000.py @@ -83,16 +83,16 @@ def bankDescriptors() -> List[Dict]: # Base bank configurations base_banks = [ - {"id": 0x00, "name": "A", "size": 100} # Adjust to 128 once skipping is implemented + {"id": 0x00, "name": "A", "size": 128} # Adjust to 128 once skipping is implemented ] if K5000_SPECIFIC_DEVICE == "K5000W": base_banks.append({"id": 0x01, "name": "B", "size": 128}) # ROMpler Bank for W if K5000_SPECIFIC_DEVICE in ["K5000S", "K5000R"]: - base_banks.append({"id": 0x02, "name": "D", "size": 40}) # Adjust to 128 once skipping is implemented + base_banks.append({"id": 0x02, "name": "D", "size": 128}) # Adjust to 128 once skipping is implemented base_banks.extend([ - {"id": 0x03, "name": "E", "size": 51}, + {"id": 0x03, "name": "E", "size": 128}, {"id": 0x04, "name": "F", "size": 128} ]) # Expansion banks @@ -303,10 +303,29 @@ def isBankDumpFinished(messages): def extractPatchesFromAllBankMessages(messages): + """ + Extracts individual patches from a bank dump. + + Parameters: + - messages (List[List[int]]): List of SysEx messages forming a bank dump. + + Returns: + - List[List[int]]: Extracted patch SysEx messages. + """ if not messages: raise ValueError("No messages received for bank dump.") - bank_byte = messages[0][7] if len(messages[0]) > 7 else 0x00 # Ensure it's a valid integer + # Get dynamic bank information + banks = bankDescriptors() + + # Determine bank ID from the first message (ensuring it is valid) + received_bank_byte = messages[0][7] if len(messages[0]) > 7 else None + selected_bank = next((b for b in banks if b["bank"] == received_bank_byte), None) + + if selected_bank is None: + raise ValueError(f"Invalid bank byte {received_bank_byte}. No matching bank found.") + + bank_byte = selected_bank["bank"] # Flatten all messages into a single data array (excluding SysEx delimiters) all_data = [] @@ -322,8 +341,8 @@ def extractPatchesFromAllBankMessages(messages): # Extract available patch numbers from the tone map patch_numbers = [i for i, present in enumerate(tone_map) if present] - patch_count = len(patch_numbers) # Number of patches present in the dump - print(f"Contains {patch_count} patches.") + patch_count = len(patch_numbers) + print(f"Contains {patch_count} patches in bank {selected_bank['name']}.") # Remaining patch data (skip tone map and padding) patch_data_start = 19 @@ -347,7 +366,6 @@ def extractPatchesFromAllBankMessages(messages): # Extract patch data dynamically patch_body = patch_data[offset:offset + bytes_left] - # print(f"Patch data is from {offset} to {offset + bytes_left} ({len(patch_body)} bytes)") if len(patch_body) < 60: # Ensure it's large enough to contain source info print(f"Error: Patch {i+1} is too small to be valid. Skipping.") @@ -356,7 +374,6 @@ def extractPatchesFromAllBankMessages(messages): # Extract source count (from Common Data byte 50) source_count_offset = 50 source_count = min(patch_body[source_count_offset], 6) # Max 6 sources - print(f"Patch {i + 1} detected {source_count} sources.") # ---- PCM/ADD Classification Per Source ---- add_count = 0 @@ -367,7 +384,6 @@ def extractPatchesFromAllBankMessages(messages): source_offset = source_type_offset + (s * SOURCE_DATA_SIZE) if source_offset + 1 >= len(patch_body): # Avoid out-of-bounds access - print(f"Patch {i+1} Source {s+1}: Warning - Not enough data for wave type, assuming PCM.") pcm_count += 1 continue @@ -400,8 +416,6 @@ def extractPatchesFromAllBankMessages(messages): f"Error: Could not determine patch size for {pcm_count} PCM, {add_count} ADD sources in Patch {i + 1}. Skipping.") continue - print(f"Patch {i + 1} calculated size from SINGLE_INFO: {patch_size}") - if patch_size <= 0 or patch_size > bytes_left + 1: print( f"Error: Invalid patch size detected for patch {i + 1}. Skipping. Expected {patch_size}, but only {bytes_left} remain.") @@ -415,27 +429,17 @@ def extractPatchesFromAllBankMessages(messages): 0xF0, KawaiSysexID, 0x00, OneBlockDump, 0x00, 0x0A, 0x00, bank_byte, patch_number, int(checksum) ] + list(current_patch) + [0xF7] - # Debug: Print full SysEx for the first patch - # if i == 0: # Only for the first patch - # full_sysex_hex = ' '.join(f'{byte:02X}' for byte in formatted_patch) - # print(f"Full SysEx for Patch 1:\n{full_sysex_hex}") - - print(f"Patch {i+1} first bytes: {' '.join(f'{byte:02X}' for byte in formatted_patch[:20])}") - - # Validate patch if not isSingleProgramDump(formatted_patch): print(f"Error: Extracted patch {i+1} is NOT a valid program dump. Skipping.") continue - # Append the valid patch patches.append(formatted_patch) # Move offset forward correctly offset += patch_size - 1 # ✅ Ensure correct alignment for next patch - # print(f"Moving offset to {offset}") - print(f"Extracted {len(patches)} valid patches.") - return patches # Return a list of lists (KnobKraft format) + print(f"Extracted {len(patches)} valid patches from bank {selected_bank['name']}.") + return patches def getToneMap(data: bytes) -> List[bool]: # ✅ !!!PCM bank has no tone map, don't yet, what to do with it