Skip to content
Mostelin edited this page Nov 27, 2021 · 49 revisions

Ensoniq ESQ-1 Adaptation

Contributors

Mark Peters

Notes

This adaptation should also work with the Ensoniq ESQ-M and Ensoniq SQ-80, which share sysex codes and program and bank structure with the ESQ-1.

Work started on 24 November 2021.

0.85 version uploaded 27 November 2021.

Status

  • Reads/writes programs from/to ESQ-1
  • Reads bank from ESQ-1
  • Imports from computer
  • Displays names extracted from programs
  • Adopts new name if a duplicate program is loaded

Not implemented

  • Saving edited names to the ESQ-1

Issues

  1. Cannot slow down auto-detect. Synth responds in ~56 milliseconds, but by then the software has already reported no synth found.
  2. Only works on channel 1.
  3. Program import often just does nothing. If this is due to duplication, a log message would help and explain.

Limitations

  • ESQ-1: writing to a specific memory location is not permitted.
  • ESQ-1: writing to or dumping cartridge banks is not permitted.

References

ESQ-1 Musician's Manual

ESQ-1 Software Version 3 Update

ESQ-M Musician's Manual

SQ-80 Musician's Manual

Code


#
#   Copyright (c) 2020 Christof Ruch. All rights reserved.
#   Ensoniq ESQ-1 version 0.85 adaptation by Mark Peters, 2021.
#   Dual licensed: Distributed under Affero GPL license by default, an MIT license is available for purchase
#
#   To do:
#   enable use of any MIDI channel, not just 00.

import hashlib

def name():
    return "Ensoniq ESQ-1"


def setupHelp():
    return "The Ensoniq ESQ-1 must be prepared to receive sysex.\n\n" \
        "1. Press MIDI button - to open MIDI settings.\n" \
        "2. Press bottom-right soft button - to select MIDI Enable features.\n" \
        "3. Press Up data-entry button - to enable SX.\n" \
        "4. Press Internal button - to select a program page.\n\n" \
        "If a program has been received from the Knobkraft ORM and is in the ESQ-1 edit buffer,\n" \
        "it must be cleared with *EXIT* or saved before another program can be received.\n\n" \
        "This adaptation should work with the ESQ-M and SQ-80 too, but this has not been tested."


def generalMessageDelay():
    return 200


def deviceDetectWaitMilliseconds():
    return 200


def createDeviceDetectMessage(channel):
    # See ESQ-1 Software Version 3 Update p 7 for Device Inquiry Request (note error regarding last byte):
    # byte 1: f0 System Exclusive status byte
    # byte 2: 7e Non-real-time message
    # byte 3: 7f All channel broadcast code
    # byte 4: 06 General Information message code
    # byte 5: 01 Device Inquiry Message message code
    # byte 6: f7 End of Exclusive
    # note this might not work for an ESQ-1 with an operating system version prior to 3.0.
    return [0b11110000, 0b01111110, 0b01111111, 0b00000110, 0b00000001, 0b11110111]


def needsChannelSpecificDetection():
    return True


def channelIfValidDeviceResponse(message):
    # See ESQ-1 Software Version 3 Update p 7 (note errors regarding last three bytes),
    # ESQ-M Musician's Manual p 111 and SQ-80 Musician's Manual p 196 for their Device ID Messages:
    if (len(message) > 15
        and message[0] == 0b11110000  # f0 Sysex
        and message[1] == 0b01111110  # 7e Non-real-time message
        and message[3] == 0b00000110  # 06 General Information message code
        and message[4] == 0b00000010  # 02 Device ID Message code
        and message[5] == 0b00001111  # 0f ENSONIQ System Exclusive manufacturer's code
        and message[6] == 0b00000010  # 02 ESQ Product Family code (lsb)
        and message[7] == 0b00000000):  # 00 ESQ Product Family code (msb)
##        and message[8] == 0b00000001  # 01 ESQ-1 Family Member code (lsb) ESQ-1: 01; ESQ-M: 10; SQ-80: 11.
##        and message[9] == 0b00000000  # 00 ESQ-1 Family Member code (msb)  
##        and message[10] == 0b00000000  # 00 Software revision information
##        and message[11] == 0b00000000  # 00 - unused - 
##        and message[12] == 0b00000110  # 32 Minor Version number (decimal fraction)
##        and message[13] == 0b00000110  # 03 Major Version number (integer portion)
##        and message[14] == 0b11110111  # f7 End of Exclusive
        return message[2] # 00-0f Base MIDI Channel
    return -1


def createEditBufferRequest(channel):
    # See ESQ-1 Musician's Manual appendix p A-9 for Current Program Dump Request:
    return [0xf0, 0x0f, 0x02, channel, 0x09, 0xf7]


def isEditBufferDump(message):
    # See ESQ-1 Musician's Manual appendix pp A-5 and A-6 for single-program header
    return (len(message) > 4
        and message[0] == 0xf0  # Sysex
        and message[1] == 0x0f  # Ensoniq
##        and message[2] == 0x02  # ESQ-1
        and message[4] == 0x01  # Single Program Dump
        )


def createProgramDumpRequest(channel, program_number):
    # See ESQ-1 Musician's Manual appendix p A-9 for Current Program Dump Request.
    # To select a specific program we send a program change in the range 0-119 to make that program current.
    # Note that if the ESQ-1 does not have a program cartridge the available range is 0-39.
    return [0xc0 | channel, program_number] + createEditBufferRequest(channel)


def isSingleProgramDump(message):
    # This is in fact the edit buffer
    return isEditBufferDump(message)


def createBankDumpRequest(channel, bank):
    # See ESQ-1 Musician's Manual appendix p A-9 for All Program Dump Request:
    return [0xf0, 0x0f, 0x02, channel, 0x0a, 0xf7]


def isPartOfBankDump(message):
    # See ESQ-1 Musician's Manual appendix pp A-5 and A-6 for all-program header
    return (len(message) > 4
        and message[0] == 0xf0  # Sysex
        and message[1] == 0x0f  # Ensoniq
##        and message[2] == 0x02  # ESQ-1
        and message[4] == 0x02  # All Program Dump
        )


def isBankDumpFinished(messages):
    for message in messages:
        if isPartOfBankDump(message):
            return True
    return False


def extractPatchesFromBank(message):
    # A bank dump consists of 8166 bytes: 5 in the header, 8160 (in 40 programs of 204), 1 in the footer
    if isPartOfBankDump(message):
        channel = message[2]
        data = message[5:-1]
        # After removing the sysex header and footer we are left with 40 programs of 204 bytes each
        data_pointer = 0
        result = []
        while data_pointer + 203 < len(data):
            # Read one more patch
            next_patch = data[data_pointer:data_pointer + 204]
            next_program_dump = [0xf0, 0x0f, 0x02, channel, 0x01] + next_patch + [0xf7]
            print("Found patch " + nameFromDump(next_program_dump))
            result = result + next_program_dump
            data_pointer = data_pointer + 204
        return result
    raise Exception("This code can only read a single message of type 'ALL DATA DUMP'")


def numberOfBanks():
    # The ESQ-1 can be fitted with a program cartridge adding two more banks of 40. Assume this is not fitted.
    # In any case, only the Internal bank of 40 is available for dumping.
    return 1


def numberOfPatchesPerBank():
    return 40


def nameFromDump(message):
    # The 6 characters of the name are encoded right after the header, two bytes per character.
    name = ''
    i = 0
    if len(message) > 17: # 5 bytes of header + 12 bytes for name.
        while i < 12: # need to deal with little-endian hex order
            a = message[i + 5]
            b = message[i + 6]
            c = a + 16 * b
            name += chr(c)
            i += 2
    return name


def convertToEditBuffer(channel, message):
    # Instead of just returning the same message, this ensures that the most recently chosen channel is used.
    if isEditBufferDump(message):
        return message[:3] + [channel] + message[4:]


def calculateFingerprint(message):
    # ignore 5 bytes of header, 12 bytes of name, and last (sysex footer) byte
    data = message[17:-1]
    return hashlib.md5(bytearray(data)).hexdigest()  # Calculate the fingerprint from the cleaned payload data


def friendlyBankName(bank_number):
    if bank_number == 1:
        return "Cartridge A"
    if bank_number == 2:
        return "Cartridge B"
    return "Internal"

Clone this wiki locally