christofmuc / KnobKraft-orm

The KnobKraft Orm - The free modern cross-platform MIDI Sysex Librarian
GNU Affero General Public License v3.0
194 stars 25 forks source link

How to use ChatGPT to write an adaptation (in this case for the Waldorf Pulse) #244

Open markusschloesser opened 1 year ago

markusschloesser commented 1 year ago

I wanted to document what I do do/did to get ChatGPT to write me an adaptation for my Waldorf Pulse (plus), to get others a starting point when using ChatGPT, so something like a tipps on which prompts work/work better. I will also of course test the results and hopefully will in the end have a working adaptation for my Waldorf Pulse.

So here's my conversation with ChatGPT (in my case GPT4 model, as I am a paying subscriber):

ME: Please provide me with a knobkraft adaptation python module for the Waldorf pulse

ChatGPT: I am unable to provide you with a ready-made KnobKraft adaptation for the Waldorf Pulse, as the exact requirements and setup may vary. However, I can guide you through the process of creating a basic Python module to interface with the synthesizer using the MIDI protocol. This will allow you to interact with the device and control its parameters.

To create a Python module for the Waldorf Pulse, you can use the 'mido' library to send and receive MIDI messages. You can install it via pip:

pip install mido Then, you can create a Python module, let's call it 'waldorf_pulse.py', and start with the following structure:

import mido

class WaldorfPulse:
    def __init__(self, input_port, output_port):
        self.input_port = input_port
        self.output_port = output_port

    def send_message(self, msg):

ME: At this point I stopped and asked "Thanks but I want a knobkraft adaptation" ChatGPT: I apologize for the confusion. KnobKraft Orm is a tool for interacting with MIDI synthesizers, and it uses Python for writing adaptations. Here's a basic outline for creating a KnobKraft adaptation for the Waldorf Pulse: First, make sure you have the KnobKraft Orm software installed. You can find it here: https://github.com/christofmuc/KnobKraft-orm Next, create a new Python file called waldorf_pulse_adaptation.py in the adaptations folder of your KnobKraft installation. Here's a starting point for the adaptation:

from typing import List, Any
from adapter import Adapter
from midimessage import MidiMessage

class WaldorfPulse(Adapter):
    def __init__(self, device_name: str, inport: str, outport: str):
        super().__init__(device_name, inport, outport)

    def device_name(self) -> str:
        return "Waldorf Pulse"

    def recognize(self, messages: List[MidiMessage]) -> bool:
        # Check if the messages received are from Waldorf Pulse by looking for a specific
        # sysex message signature. Adjust this based on the actual sysex message expected.
        for msg in messages:
            if msg.is_sysex() and msg.sysex_data()[:5] == [0x7D, 0x00, 0x01, 0x02, 0x03]:
                return True
        return False

    def patch_from_sysex(self, sysex_data: List[int]) -> Any:
        # Parse the sysex data and return a dictionary representing the patch.
        # You'll need to implement this based on the Waldorf Pulse sysex format.
        pass

    def sysex_from_patch(self, patch_data: Any) -> List[int]:
        # Create a sysex message from the patch data.
        # You'll need to implement this based on the Waldorf Pulse sysex format.
        pass

    def queries(self) -> List[MidiMessage]:
        # Return a list of MIDI messages to query the device for patch data.
        # For the Waldorf Pulse, this might be a sysex message requesting a patch dump.
        pass

The provided outline needs to be completed according to the Waldorf Pulse sysex format and the specific MIDI messages that the device recognizes. Please consult the device's manual for details on the sysex format and the MIDI implementation.

ME: Do you know the sysex format for the Waldorf pulse? CHATGPT: Yes, I can provide you with the basic System Exclusive (SysEx) format for the Waldorf Pulse synthesizer. The SysEx format consists of the following structure:

SysEx Start: 0xF0 Waldorf Pulse Manufacturer ID: 0x3E (62 in decimal) Device ID: typically 0x0E (14 in decimal) for Waldorf Pulse, but can be any valid Device ID between 0x00 and 0x7F Model ID: 0x02 (2 in decimal) for Waldorf Pulse Command ID: Represents the specific command, such as requesting a patch dump, or sending patch data Data: The data bytes corresponding to the command SysEx End: 0xF7 Here's an example of a SysEx message requesting a patch dump from the Waldorf Pulse:

F0 3E 0E 02 44 F7 Where:

F0: SysEx Start 3E: Waldorf Pulse Manufacturer ID 0E: Device ID (Waldorf Pulse) 02: Model ID (Waldorf Pulse) 44: Command ID (Request Patch Dump) F7: SysEx End You'll need to implement the specific SysEx commands for your Waldorf Pulse adaptation, such as parsing and creating patch data. Please consult the Waldorf Pulse manual for more details on the SysEx implementation, including the command IDs and data formats for different commands.

ME: Now please use your knowledge of the sysex for the Waldorf pulse and change the knobkraft adaptation. Please be aware that knobkraft adaptations are not based on classes CHATGPT: I apologize for the confusion earlier. Here's a revised KnobKraft adaptation for the Waldorf Pulse synthesizer without using classes. Create a new Python file called waldorf_pulse_adaptation.py in the adaptations folder of your KnobKraft installation.

from adapter import Adapter
from midimessage import MidiMessage
from typing import List, Tuple

def name() -> str:
    return "Waldorf Pulse"

def create_device(inport: str, outport: str) -> Tuple[str, str]:
    return inport, outport

def recognize(device_name: str, messages: List[MidiMessage]) -> bool:
    for msg in messages:
        if msg.is_sysex() and msg.sysex_data()[:4] == [0x3E, 0x0E, 0x02, 0x44]:
            return True
    return False

def request_patch(device_name: str, inport: str, outport: str, patch_number: int) -> List[MidiMessage]:
    msg_data = [0xF0, 0x3E, 0x0E, 0x02, 0x44, 0xF7]
    return [MidiMessage.from_bytes(msg_data)]

def patch_from_sysex(device_name: str, msg: MidiMessage) -> Tuple[int, str, bytes]:
    if msg.is_sysex() and msg.sysex_data()[:4] == [0x3E, 0x0E, 0x02, 0x45]:
        data = msg.sysex_data()
        patch_number = data[5]
        patch_name = "".join([chr(x) for x in data[6:6 + 10]])
        return patch_number, patch_name, bytes(data)
    return None

def sysex_from_patch(device_name: str, data: bytes) -> MidiMessage:
    return MidiMessage.from_bytes(data)

Adapter.register(name, create_device, recognize, request_patch, patch_from_sysex, sysex_from_patch)

This adaptation should work with the Waldorf Pulse synthesizer. However, it is a simplified version, and you may need to adjust it based on your specific device, patch format, or other requirements.

Please note that the code assumes the SysEx message for requesting a patch dump is F0 3E 0E 02 44 F7, and the SysEx message for receiving patch data starts with F0 3E 0E 02 45. Additionally, it assumes that the patch number is the 6th byte and the patch name is in the next 10 bytes in the SysEx message. If your device has a different SysEx implementation, you'll need to adjust the code accordingly.

ME: This is the source code for another synth for a knobkraft adaptation, please use the same style for a Waldorf pulse adaptation: (copy pasted source code of existing Kijimi adaptation) CHATGPT: Here's a KnobKraft adaptation for the Waldorf Pulse synthesizer based on the provided example. Create a new Python file called waldorf_pulse_adaptation.py in the adaptations folder of your KnobKraft installation.

import hashlib
import re

wp_manufacturer_id = 0x3E
wp_device_id = 0x0E
wp_model_id = 0x02
command_request_patch = 0x44
command_single_program_dump = 0x45

def name():
    return "Waldorf Pulse"

def createDeviceDetectMessage(channel):
    return [0xF0, wp_manufacturer_id, wp_device_id, wp_model_id, command_request_patch, 0xF7]

def needsChannelSpecificDetection():
    return False

def channelIfValidDeviceResponse(message):
    if len(message) > 4 and message[:5] == [0xF0, wp_manufacturer_id, wp_device_id, wp_model_id, command_request_patch]:
        return 1
    return -1

def numberOfBanks():
    return 1

def numberOfPatchesPerBank():
    return 100

def createProgramDumpRequest(channel, program_no):
    return [0xF0, wp_manufacturer_id, wp_device_id, wp_model_id, command_request_patch, program_no, 0xF7]

def isSingleProgramDump(message):
    return len(message) > 5 and message[:5] == [0xF0, wp_manufacturer_id, wp_device_id, wp_model_id, command_single_program_dump]

def convertToProgramDump(channel, message, program_no):
    if isSingleProgramDump(message):
        return message
    raise Exception("Can only convert singleProgramDumps")

def nameFromDump(message):
    if isSingleProgramDump(message):
        return "Pulse %03d" % message[5]
    raise Exception("Only single program dumps can be named")

def isDefaultName(patchName):
    return re.match("Pulse [0-9][0-9][0-9]$", patchName) is not None

def numberFromDump(message):
    if isSingleProgramDump(message):
        return message[5]
    raise Exception("Only single program dumps have program numbers")

def calculateFingerprint(message):
    if isSingleProgramDump(message):
        return hashlib.md5(bytes(message)).hexdigest()
    raise Exception("Can only fingerprint singleProgramDumps")

def run_tests():
    pass

if __name__ == "__main__":
    run_tests()

This adaptation should work with the Waldorf Pulse synthesizer. However, it is a simplified version, and you may need to adjust it based on your specific device, patch format, or other requirements.

ME: How similar are the Waldorf blofeld and the Waldorf pulse in terms of sysex? (because there is an existing adaptation for the blofeld) CHATGPT: While both the Waldorf Blofeld and Waldorf Pulse are synthesizers from the same manufacturer, their System Exclusive (SysEx) implementations can differ significantly. Here's a brief comparison of their SysEx formats:

Waldorf Pulse: SysEx Start: 0xF0 Manufacturer ID: 0x3E Device ID: 0x0E Model ID: 0x02 Command ID: Represents the specific command Data: The data bytes corresponding to the command SysEx End: 0xF7

Waldorf Blofeld: SysEx Start: 0xF0 Manufacturer ID: 0x3E Device ID: 0x13 (for Blofeld) Command ID: Represents the specific command Data: The data bytes corresponding to the command SysEx End: 0xF7 As you can see, the basic structure of the SysEx messages is similar, but there are some key differences, such as the Device ID and Model ID. Additionally, the Command IDs and data formats for different commands can vary between the two synthesizers.

To adapt a KnobKraft adaptation from the Waldorf Pulse to the Waldorf Blofeld, you would need to adjust the Device ID, Model ID, and command-specific details to match the Blofeld's SysEx implementation. Consult the Waldorf Blofeld manual for more details on the SysEx commands and data formats specific to that synthesizer.

ME: Which of the following sysex features does the Waldorf pulse support: 1 Synth name 2 numberOfBanks 3 numberOfPatchesPerBank 4 bankDescriptors 5 createDeviceDetectMessage 6 channelIfValidDeviceResponse 7 needsChannelSpecificDetection 8 deviceDetectWaitMilliseconds 9 Edit Buffer Capability 10 isPartOfEditBufferDump 11 Program Dump Capability 12 isPartOfSingleProgramDump 13 Bank Dump Capability 14 numberFromDump 15 nameFromDump 16 generalMessageDelay 17 renamePatch 18 isDefaultName 19 calculateFingerprint 20 friendlyBankName 21 friendlyProgramName 22 Layer Capability 23 setLayerName 24 setupHelp (from knobkraft adaptation implementation) CHATGPT: Based on the Waldorf Pulse manual, here are the SysEx features supported by the synthesizer:

Synth name: No, there is no specific SysEx message to request or set the synth name. numberOfBanks: Yes, Waldorf Pulse has a single bank. numberOfPatchesPerBank: Yes, there are 100 patches per bank. bankDescriptors: No, there is only one bank, so no specific bank descriptors are necessary. createDeviceDetectMessage: Yes, you can create a request patch message to detect the device. channelIfValidDeviceResponse: Yes, if the response is valid, you can return the channel. needsChannelSpecificDetection: No, the Pulse doesn't need channel-specific detection. deviceDetectWaitMilliseconds: Not mentioned in the manual, but adding some wait time may help. Edit Buffer Capability: No, Waldorf Pulse does not have an edit buffer. isPartOfEditBufferDump: Not applicable, as there is no edit buffer. Program Dump Capability: Yes, the Pulse can send and receive single program dumps. isPartOfSingleProgramDump: Yes, you can detect if a message is part of a single program dump. Bank Dump Capability: No, Waldorf Pulse does not support bank dump messages. numberFromDump: Yes, you can extract the program number from a program dump message. nameFromDump: No, Waldorf Pulse does not have patch names stored in its SysEx messages. generalMessageDelay: Not mentioned in the manual, but adding some wait time may help. renamePatch: Not applicable, as the Pulse does not support patch names. isDefaultName: Not applicable, as the Pulse does not support patch names. calculateFingerprint: Yes, you can calculate a fingerprint using the program data. friendlyBankName: Not applicable, as there is only one bank. friendlyProgramName: Not applicable, as the Pulse does not support patch names. Layer Capability: No, the Pulse is a monophonic synthesizer and does not have layer capabilities. setLayerName: Not applicable, as the Pulse does not have layer capabilities. setupHelp: This is not a SysEx feature but a part of the KnobKraft adaptation implementation. Please note that while the above list is based on the available Waldorf Pulse manual, some details might not be accurate or complete. Always consult the manual for the most up-to-date information about your specific device.

ME: Please provide me with a knobkraft adaptation for all of the supporter sysex features CHATGPT: Here's a KnobKraft adaptation for the Waldorf Pulse synthesizer, covering the supported SysEx features. Create a new Python file called waldorf_pulse_adaptation.py in the adaptations folder of your KnobKraft installation.

import hashlib

wp_manufacturer_id = 0x3E
wp_device_id = 0x0E
wp_model_id = 0x02
command_request_patch = 0x44
command_single_program_dump = 0x45

def name():
    return "Waldorf Pulse"

def createDeviceDetectMessage(channel):
    return [0xF0, wp_manufacturer_id, wp_device_id, wp_model_id, command_request_patch, 0xF7]

def needsChannelSpecificDetection():
    return False

def channelIfValidDeviceResponse(message):
    if len(message) > 4 and message[:5] == [0xF0, wp_manufacturer_id, wp_device_id, wp_model_id, command_request_patch]:
        return 1
    return -1

def numberOfBanks():
    return 1

def numberOfPatchesPerBank():
    return 100

def createProgramDumpRequest(channel, program_no):
    return [0xF0, wp_manufacturer_id, wp_device_id, wp_model_id, command_request_patch, program_no, 0xF7]

def isSingleProgramDump(message):
    return len(message) > 5 and message[:5] == [0xF0, wp_manufacturer_id, wp_device_id, wp_model_id, command_single_program_dump]

def convertToProgramDump(channel, message, program_no):
    if isSingleProgramDump(message):
        return message
    raise Exception("Can only convert singleProgramDumps")

def numberFromDump(message):
    if isSingleProgramDump(message):
        return message[5]
    raise Exception("Only single program dumps have program numbers")

def calculateFingerprint(message):
    if isSingleProgramDump(message):
        return hashlib.md5(bytes(message)).hexdigest()
    raise Exception("Can only fingerprint singleProgramDumps")

def run_tests():
    pass

if __name__ == "__main__":
    run_tests()

This adaptation should work with the Waldorf Pulse synthesizer, considering the supported SysEx features. Note that some features, such as patch names, are not available on the Waldorf Pulse. You may need to adjust the adaptation based on your specific device or requirements.

I will now go ahead and compare and test the code and then document here what already works and what not.

markusschloesser commented 1 year ago

So obviously the first provided code couldn't work, so I neglected it. The second looked a bit better, but imported strange things. The third wasn't aware that the Pulse does not have names for patches. The forth I tested, but it didn't work. Checking the sysex spec, it became clear that the sysex for a couple of things were wrong (Pulse Machine ID should be 0x0B). So I started feeding the sysex spec to ChatGPT

ME: this doesnt work: Here are the sysex commands from the manual: 1.) Program Dump Byte Nr. Value Description 0 $F0 Start of System Exclusive 1 $3E Waldorf Electronics GmbH Manufacturer ID 2 $0B Pulse Machine ID 3 DEV Entsprich dem Globalparameter Device ID, s. 5.6 4 $00 Dump Typ, Hier Program Dump 5 PRG Program Number 0-99 fuer P.1 bis P.99 und P.rn 2.) Program Bulk Dump Byte Nr. Value Description 0 $F0 Start of System Exclusive 1 $3E Waldorf Electronics GmbH Manufacturer ID 2 $0B Pulse Machine ID 3 DEV Entsprich dem Globalparameter Device ID, s. 5.6 4 $01 Dump Typ, Hier Program Bulk Dump 5 PRG Program Number 0-39 fuer P.1 bis P.40 4.) Program Dump Request Byte Nr. Value Description 0 $F0 Start of System Exclusive 1 $3E Waldorf Electronics GmbH Manufacturer ID 2 $0B Pulse Machine ID 3 DEV Entsprich dem Globalparameter Device ID, s. 5.6 4 $40 Dump Typ, Hier Program Dump Request 5 PRG Program Number 0-99 fuer P.1 bis P.99 und P.rn 6 $F7 End of System Exclusive 5.) Program Bulk Dump Request Byte Nr. Value Description 0 $F0 Start of System Exclusive 1 $3E Waldorf Electronics GmbH Manufacturer ID 2 $0B Pulse Machine ID 3 DEV Entsprich dem Globalparameter Device ID, s. 5.6 4 $41 Dump Typ, Hier Program Bulk Dump Request 5 PRG Program Number 0-99 fuer P.1 bis P.99 und P.rn 6 $F7 End of System Exclusive

CHATGPT:I apologize for the confusion. I've revised the KnobKraft adaptation for the Waldorf Pulse synthesizer, taking the SysEx commands from the manual into account:

import hashlib

wp_manufacturer_id = 0x3E
wp_device_id = 0x0B
command_request_patch = 0x40
command_single_program_dump = 0x00
command_bulk_program_dump = 0x01

def name():
    return "Waldorf Pulse"

def createDeviceDetectMessage(channel):
    return [0xF0, wp_manufacturer_id, wp_device_id, channel, command_request_patch, 0x00, 0xF7]

def needsChannelSpecificDetection():
    return True

def channelIfValidDeviceResponse(message):
    if len(message) > 5 and message[:5] == [0xF0, wp_manufacturer_id, wp_device_id, message[3], command_single_program_dump]:
        return message[3]
    return -1

def numberOfBanks():
    return 1

def numberOfPatchesPerBank():
    return 100

def createProgramDumpRequest(channel, program_no):
    return [0xF0, wp_manufacturer_id, wp_device_id, channel, command_request_patch, program_no, 0xF7]

def isSingleProgramDump(message):
    return len(message) > 5 and message[:5] == [0xF0, wp_manufacturer_id, wp_device_id, message[3], command_single_program_dump]

def convertToProgramDump(channel, message, program_no):
    if isSingleProgramDump(message):
        return message
    raise Exception("Can only convert singleProgramDumps")

def numberFromDump(message):
    if isSingleProgramDump(message):
        return message[5]
    raise Exception("Only single program dumps have program numbers")

def calculateFingerprint(message):
    if isSingleProgramDump(message):
        return hashlib.md5(bytes(message)).hexdigest()
    raise Exception("Can only fingerprint singleProgramDumps")

def run_tests():
    pass

if __name__ == "__main__":
    run_tests()

This updated adaptation considers the correct SysEx commands for the Waldorf Pulse. Please try this version and let me know if you encounter any issues.

This still doesn't work (probably because I have a different device ID), but I CAN receive a manual program dump. A manual bulk dump doesn't work, error message "Ignoring sysex message found, not implemented:" (because bulks have a different sysex 0x01 instead of 0x00)

christofmuc commented 1 year ago

@markusschloesser Crazy stuff, really cool. I would have suggested to feed the GPT with my adaptation programming manual in the first place - it will not know the latest version, and generally it is good to preset the attention of the model to the already known information. It would probably not have had the first try doing something completely different. Giving it an example code is also a great move, that seems to have improved it a lot!

I have only seen videos so far on the code plugin where it can run python code on its own - as we test the adaptations outside of the C++ application anyway, that looks like a cool short cut to getting adaptations! Maybe it can also help us with making better tests.

Generally, the adaptations are short enough to easily be understood by the model, which is a good thing, as I am sure it would chew a bit on the C++ code of KnobKraft itself. Let's wait a year or two for that to work!

markusschloesser commented 1 year ago

@christofmuc I couldn't feed it the adaptation guideline before, because it is too long, however now it can access the web itself, so continuing: ME: this is the programming guide on how to write a knobkraft adaptation https://github.com/christofmuc/KnobKraft-orm/blob/master/adaptions/Adaptation%20Programming%20Guide.md please check if your last version fulfills all that is needed CHATGPT: Thank you for the guide. According to it, the following are the required steps:

The file needs to be in the "adaptions" directory of the package. The file needs to have a specific set of functions defined, some of which are optional, and all functions are global and not class methods. Given these requirements, I have revised the KnobKraft adaptation for the Waldorf Pulse synthesizer:

import hashlib

wp_manufacturer_id = 0x3E
wp_device_id = 0x0B
command_request_patch = 0x40
command_single_program_dump = 0x00
command_bulk_program_dump = 0x01

def module_id():
    # ID for this module
    return "waldorf_pulse"

def module_name():
    # Name for this module
    return "Waldorf Pulse"

def module_version():
    # Version string for this module
    return "1.0"

def device_id():
    # ID for the synth this module supports
    return wp_device_id

def createDeviceDetectMessage(channel):
    # Message to detect the device
    return [0xF0, wp_manufacturer_id, wp_device_id, channel, command_request_patch, 0x00, 0xF7]

def expectedDeviceDetectResponse(message):
    # Expected response to the detection message
    if len(message) > 5 and message[:5] == [0xF0, wp_manufacturer_id, wp_device_id, message[3], command_single_program_dump]:
        return True
    return False

def number_of_banks():
    # Waldorf Pulse has only 1 bank
    return 1

def number_of_patches():
    # 100 patches per bank
    return 100

def createEditBufferRequest(channel):
    # Not supported by the device
    pass

def createEditBufferWrite(channel, buffer):
    # Not supported by the device
    pass

def createProgramWriteRequest(channel, program):
    # Write a program to the device
    return [0xF0, wp_manufacturer_id, wp_device_id, channel, command_request_patch, program, 0xF7]

def createProgramDumpRequest(channel, program_number):
    # Request a program dump
    return [0xF0, wp_manufacturer_id, wp_device_id, channel, command_request_patch, program_number, 0xF7]

def isProgramDumpResponse(message):
    # Check if the message is a program dump
    return len(message) > 5 and message[:5] == [0xF0, wp_manufacturer_id, wp_device_id, message[3], command_single_program_dump]

def extractProgramDumpData(message):
    # Extract program dump data from the message
    if isProgramDumpResponse(message):
        return message[5:-1]
    raise Exception("Invalid program dump response")

def createProgramFromData(channel, data):
    # Create a program from data
    return [0xF0, wp_manufacturer_id, wp_device_id, channel, command_single_program_dump] + data + [0xF7]

def calculateFingerprint(message):
    # Calculate the fingerprint of the message
    return hashlib.md5(bytes(message)).hexdigest()

def setupHelp():
    # Return a help message for setup
    return """To setup the Waldorf Pulse for SysEx communication,
1. Go to the Global settings and set the 'Device ID' to the MIDI channel you want the Pulse to respond on.
2. Set the 'SysEx Enable' to ON.
"""

Please note that certain operations are not supported by Waldorf Pulse, such as edit buffer operations. However, the basic program dump and write operations are supported.

I will test

markusschloesser commented 1 year ago

still testing with the old version, cos the last one seems stupid.

  1. When I send Out Pulse+ DIN 4 Sysex [f0 3e 0b 7f 40 00 f7] (which is the "Single Program Dump Request") the pulse replies with In Pulse+ DIN 4 Sysex [f0 3e 0b 21 00 00 40 30 01 40 40 4e 01 28 00 01 34 40 01 19 19 19 00 44 00 3d 06 00 32 64 40 40 02 00 28 31 32 40 02 40 04 00 01 02 01 41 01 01 3f 05 00 40 00 00 40 0a 00 03 0e 03 00 1c 60 7f 40 40 04 00 40 40 40 00 00 40 00 01 f7] But KK doesn't show anything. Is this supposed to work for single programs? Current code for this use case is:
def createProgramDumpRequest(channel, program_no):
    return [0xF0, wp_manufacturer_id, machine_ID, 127, Program_Dump_Request, program_no, 0xF7]

def isSingleProgramDump(message):
    return len(message) > 5 and message[:5] == [0xF0, wp_manufacturer_id, machine_ID, device_ID, is_single_program_dump, message[3]]
  1. the pulse has a bulk program request which will return single messages but with a one byte difference. However one must still iterate over the single programs IMHO image

    (sorry for picture posting, copy and paste from pdfs is a mess)

So how do I tell kk to go over those programs? currently the code (from chatgpt) just uses bank

  1. I added quite a bit of code wrt to bulk dumps (too long right now to post all here but:
  2. I am constantly running into the "Error calling extractPatchesFromBank: TypeError: object of type 'int' has no len()" error. I tried with message and messages. current code attached and any help appreciated! :-) waldorf_pulse.zip also the sysex spec of the Pulse: Sysex.pdf
christofmuc commented 1 year ago

I think it looks not too bad, but your isSingleProgramDump() has a weird message[3] as the last element in the list. Where does that came from? A correct detection based on the Waldorf Blofeld e.g. would look like this:


def isSingleProgramDump(message):
    return (len(message) > 5
            and message[0] == 0xf0
            and message[1] == 0x3e  # Waldorf
            and message[2] == 0x0B  # Pulse
            and message[4] == 0x00  # Sound Dump Data
            )
markusschloesser commented 1 year ago

thanks!

but your isSingleProgramDump() has a weird message[3] as the last element in the list.

Do you mean machine_ID? That's the "Device ID" from the spec, I just renamed it because "Device ID" (how it is called in the spec) is too similar to "Model ID" for my taste. I am also pretty sure, that this is needed, the replying message structure for Program Dump and Program Bulk Dump both mention that.

christofmuc commented 1 year ago

Looking more into the documentation, I really don't get the difference between a program dump and the bulk dump. We don't need the bulk dump, as implementing only the program dump will make KK iterate over all programs when doing a bank download, which sounds like what you want?

GPT4 says: One point to note from your data: It appears that the Program Bulk Dump message for the Pulse handles programs numbered 0-39 (P.1 to P.40), while the Program Dump message handles programs numbered 0-99 (P.1 to P.99 and P.rn). This suggests that the Pulse's memory is organized into blocks of 40 programs each, and a Program Bulk Dump message represents one such block.

Also, looking at the data you provided, there doesn't appear to be any notable difference between the Program Dump and Program Bulk Dump beyond the program number range. They both contain the same parameters for each program, and the data format for each parameter is the same.

christofmuc commented 1 year ago

I tried my version of isSingleProgramDump(), and it allows me to load your example program.

markusschloesser commented 1 year ago

Also, looking at the data you provided, there doesn't appear to be any notable difference between the Program Dump and Program Bulk Dump beyond the program number range. They both contain the same parameters for each program, and the data format for each parameter is the same.

they do differ! That's why I started implementing the bulk dump in the first place :-) message[4] is either 0x00 or 0x01 in the replies.

The question is, who is better to iterate? The Pulse or KK. If Pulse then we use Program_Bulk_Dump_Request = 0x41 , if KK then Program_Dump_Request = 0x40 But as I didn't (and still don't) understand HOW KK iterates over the programs, I used the bulk one. So how do I tell KK to iterate over the possible programs?

I tried my version of isSingleProgramDump(), and it allows me to load your example program.

Could you post it?

Also, could please point me into the right direction wrt to my point 4? (message/messages)? I am trying to learn :-)

christofmuc commented 1 year ago

Ok, my version was up there. You have message[3] as the 5th item in the compare list, so you compare if the program number (stored at position 5) matches your device ID. That doesn't make any sense. Correct is this:

def isSingleProgramDump(message):
    return (len(message) > 5
            and message[0] == 0xf0
            and message[1] == 0x3e  # Waldorf
            and message[2] == 0x0B  # Pulse
            and message[4] == 0x00  # Sound Dump Data
            )

The Librarian class in C++, which drives the adaptations, has a different mode for bulk dump and program dump. It will prepare bulk dump, if that is implemented.

Program dump: Just iterate over all programs in a bank, send the program dump request and expect a message back (or multiple, if you use the isPartOfProgramDump extension).

Bulk dump: Will issue a bulk dump request, and then collect all messages that test positive with isPartOfBulkDump. Then extractPatchesFromBulkDump will be called for each message individually, so really it should be message as a parameter and not messages. The reason is that this was implemented for old synths like the Kawai K3, which will send you one message which contains all 64 patches, so extractPatchesFromBulk would be called once, and return 64 patches.

Our problems come from the fact that Python does not have strong typing, so you cannot easily see if you have a list of bytes, a byte, or a list of list of bytes. And I am not consistent in the programm guide, we had that while discussing #100.

You can check in Python with isinstance() if something is a list or a number, or a different type. This could help you find these problems faster in asserting your assumptions (https://www.programiz.com/python-programming/methods/built-in/isinstance).

christofmuc commented 1 year ago

But maybe we are mistaken. If the program bulk request would make the Pulse reply with all messages, why would we need to specify the program to dump in position 5 of the bulk request? You put in the bank, but the documentation says program 0-99?

markusschloesser commented 1 year ago

Ok, my version was up there.

Sorry, I read Blofeld in the previous sentence and assumed it was the code for the Blofeld.

You put in the bank

It wasn't me! It was ChatGPT :-) I just didn't know how to do it differently. So how should I do it?

so really it should be message as a parameter and not messages

I also tried with message, didn't work either, but will test later with your version of isSingleProgramDump

Bulk dump: Will issue a bulk dump request, and then collect all messages that test positive with isPartOfBulkDump. Then extractPatchesFromBulkDump will be called for each message individually,

I understand that, but for me from a logical perspective, if a synth has a bulk dump request capability, I would use (might not be the best idea, but that was my thinking), even if a synth replies with multiple messages. Also I got the impression from the adaptation guideline, that this is the way to do it. Apologies if I am mistaken.

christofmuc commented 1 year ago

Alles gut - sometimes it is a tactical decision whether to implement all capabilities or whether it is sufficient to do less. Given the the bulk and the non-bulk messages actually are identical according to the PDF except for the code, we could probably treat them interchangeably as program dumps, and just get rid of the whole bulk dump code. Let KK do the looping, that usually works well when the message delay is high enough.

I made had GPT4 make an adaptation, not sure if it will work but here it is: (Synth is renamed so it doesn't collide with your adaptation) Waldorf_Pulse_by_GPT4.zip

markusschloesser commented 1 year ago

Given the the bulk and the non-bulk messages actually are identical according to the PDF except for the code, we could probably treat them interchangeably as program dumps, and just get rid of the whole bulk dump code. Let KK do the looping, that usually works well when the message delay is high enough.

I am fine either way :-)

I made had GPT4 make an adaptation, not sure if it will work but here it is: (Synth is renamed so it doesn't collide with your adaptation)

Unfortunately this doesn't work at all, neither detecting, nor import error Synth Waldorf Pulse GPT4 has neither HasBankDescriptorsCapability nor HasBanksCapability implemented, can't fill import banks dialog. Trying to receive a manual dump, KK receives it than actually crashes KK. Cannot even open it anymore. Only deleting the db fixes that (but I did not use a fresh db). Starting with fresh db file, trying to receive a manual dump again, kk does not receive it (even though Midi-OX does show the sysex). 🤪?

Andy2No commented 1 year ago

FWIW, I started making an adaptation for the Waldorf Pulse II, a while ago, also based on the Blofeld one. The problem I had, was that it never responds when asked for a blank / init patch, so importing patches from the synth just hangs unless every single patch slot is full, and there is no way to set a timeout, to allow the Orm to continue past that point.

christofmuc commented 1 year ago

@Andy2No I remember the timeout problem, and had started working on a solution. But then the solution wouldn't compile for old Macs I still support, so it went onto the shelf. Sigh.

Andy2No commented 1 year ago

@christofmuc I think ChatGPT is likely to fail whenever the available documentation for the synth doesn't correspond properly to how the synth really behaves, in some circumstances. I suppose it could get to a point where it can comprehend what software does, form the source code, but I can't really picture it coping well with missing information.

Thanks for trying to get a timeout working. I guess it's going to be fairly rare for it to be needed... hopefully.

markusschloesser commented 6 months ago

important information: It does have an edit buffer, but it is called/named differently ("Controller Dump"), the sysex is $F0 $3E $0B 0...126 (Device ID) $4B $F7

Info according to SoS: "My only complaint was that I thought you ought to be able to send all current controller values, but it seems that this too has been thought of — Waldorf tell me you simply carry out a Program dump request with the header string $4b to do just this." https://www.soundonsound.com/reviews/waldorf-pulse

and 4b is the Controller Dump in the manual

UPDATE: the Pulse replies to this with CCs NOT with Sysex

markusschloesser commented 6 months ago

question @christofmuc the Pulse does have a Channel/ Device ID which must be used (and can be set on the Device), this is not the Midi channel, but a value between 0 -127, whereas 127 is the "broadcast" Device ID, meaning every Pulse will respond to that. (127 cannot be used because if I send that, Ableton will start recoding for some strange reason, I had to find out the hard way when I tested a maxforlive device for the Pulse.) So i can send 0xF0, 0x3E, 0x0B, 0x7f, 0x48 0xF7 (which represents 0xF0, wp_manufacturer_id, machine_ID, broadcast_request, Global_Parameter_Dump_Request, 0xF7) and the Pulse will indeed will reply with F0 3E 0B 21 08 2C 40 21 01 21 2F F7 and the 4th byte is the Device ID I am searching for. Can / should I still use the channelIfValidDeviceResponse function for this, or rather something else?

christofmuc commented 6 months ago

@markusschloesser Two options: If the device ID is known to be from 0 to 15, you can return itfrom channelIfValidDeviceResponse instead of the MIDI channel. The only difference is the macro/masterkeyboard function which needs the real MIDI channel, but the pulse has no keyboard...

Second option is to define a global and store the detected ID in there. See the Blofeld adaptation which uses this pattern (search for the keyword global).

markusschloesser commented 6 months ago

@markusschloesser Two options: If the device ID is known to be from 0 to 15, you can return itfrom channelIfValidDeviceResponse instead of the MIDI channel. The only difference is the macro/masterkeyboard function which needs the real MIDI channel, but the pulse has no keyboard...

Second option is to define a global and store the detected ID in there. See the Blofeld adaptation which uses this pattern (search for the keyword global).

Thanks! As the possible values are 0-127 whereas 127 is broadcast, I will use the global. BTW the behavior of the Blofeld sounds very similar, looking at the adaptation, maybe you can reuse the detect function from the Pulse?

christofmuc commented 6 months ago

Sure! The Pulse is not finished yet though?

markusschloesser commented 6 months ago

Sure! The Pulse is not finished yet though?

nope, but working on it (again)! 😂 but using ID 127 as broadcast and then checking the 4th byte for the reply, can be reused immediately :-)