-
Notifications
You must be signed in to change notification settings - Fork 38
Ensoniq ESQ 1
Mostelin edited this page Nov 27, 2021
·
49 revisions
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.
- 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
- Saving edited names to the ESQ-1
- Cannot slow down auto-detect. Synth responds in ~56 milliseconds, but by then the software has already reported no synth found.
- Only works on channel 1.
- Program import often just does nothing. If this is due to duplication, a log message would help and explain.
- ESQ-1: writing to a specific memory location is not permitted.
- ESQ-1: writing to or dumping cartridge banks is not permitted.
ESQ-1 Software Version 3 Update
#
# 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"