TOOL: Generate OSC Input Profile XML with Python

This is a place for sharing with the community the results you achieved with QLC+, as a sort of use case collection.
You can share photos, videos, personal hardware/software projects, interesting HOWTO that might help other users to achieve great results.
Post Reply
strauberry
Posts: 1
Joined: Fri Jun 14, 2024 1:35 am
Real Name: Tyler

Hi, I'm not sure who needs this out there... but I sure did.

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}")
User avatar
GGGss
Posts: 2949
Joined: Mon Sep 12, 2016 7:15 pm
Location: Belgium
Real Name: Fredje Gallon

Welcome to the forum,

And THANK YOU for this welcome contribution.
All electric machines work on smoke... when the smoke escapes... they don't work anymore
Post Reply