Huge shoutout to the QLC+ forum user @janosvitok who wrote a post in 2017 detailing how the hashing function works. I couldn't have cracked this problem without their contribution.
The Python script below generates an Input Profile XML with all the correctly computed hashes derived from the OSC address.
If you have to manually create a ton of mappings in your project for some weird reason like I did... well here ya go.
RELATED INFO:
https://bugreports.qt.io/browse/QTBUG-8595
viewtopic.php?t=12587
PYTHON SCRIPT:
Code: Select all
import xml.etree.ElementTree as ET
from xml.dom import minidom
def reflect_bits(data, width):
reflection = 0
for i in range(width):
if data & (1 << i):
reflection |= (1 << ((width - 1) - i))
return reflection
def crc16_qt(data):
crc = 0xFFFF # Initial value
polynomial = 0x1021
for byte in data:
byte = reflect_bits(byte, 8)
crc ^= (byte << 8)
for _ in range(8):
if crc & 0x8000:
crc = (crc << 1) ^ polynomial
else:
crc = crc << 1
crc &= 0xFFFF # Ensure CRC remains a 16-bit value
crc = reflect_bits(crc, 16)
crc ^= 0xFFFF
return crc
def get_hash(path):
data = path.encode('utf-8')
hash_value = crc16_qt(data)
return hash_value
def create_input_profile_xml(osc_paths, types, output_file_path):
input_profile = ET.Element('InputProfile', xmlns="http://www.qlcplus.org/InputProfile")
# Create and append Creator element
creator = ET.SubElement(input_profile, 'Creator')
ET.SubElement(creator, 'Name').text = 'Q Light Controller Plus'
ET.SubElement(creator, 'Version').text = '4.13.0'
ET.SubElement(creator, 'Author').text = 'StraubNet'
# Add Manufacturer and Model
ET.SubElement(input_profile, 'Manufacturer').text = 'Ableton'
ET.SubElement(input_profile, 'Model').text = 'Live'
ET.SubElement(input_profile, 'Type').text = 'OSC'
# Create and append Channel elements
for path, type_ in zip(osc_paths, types):
channel_number = get_hash(path)
channel = ET.SubElement(input_profile, 'Channel', Number=str(channel_number))
ET.SubElement(channel, 'Name').text = path
ET.SubElement(channel, 'Type').text = type_
# Pretty print the XML
xml_str = ET.tostring(input_profile, encoding='unicode')
pretty_xml = minidom.parseString(xml_str).toprettyxml(indent=" ")
# Write to output file
with open(output_file_path, 'w', encoding='utf-8') as f:
f.write(pretty_xml)
# Example usage
osc_paths = [
'/QLC/MSTRPAR', '/QLC/CUE/4', '/QLC/MSTRUV', '/QLC/MSTRSTAGE',
'/QLC/GRANDMSTR', '/QLC/CUE/3', '/QLC/MSTRSYN', '/QLC/CUE/2',
'/QLC/CUE/1', '/QLC/HAZE', '/QLC/MSTRMVR', '/QLC/MSTRBEAMS'
]
types = [
'Slider', 'Button', 'Slider', 'Slider', 'Slider', 'Button', 'Slider',
'Button', 'Button', 'Slider', 'Slider', 'Slider'
]
output_file_path = 'QLC_Input_Profile_Generated.xml'
create_input_profile_xml(osc_paths, types, output_file_path)
print(f"Generated QLC+ Input Profile XML saved to {output_file_path}")