Skip to content

Commit

Permalink
Adding a beta version of the Yamaha DX7 implementation. The original …
Browse files Browse the repository at this point in the history
…DX7 is really restricted in that it does not implement any MIDI requests, you need the manual dump function, and it has the ugly packed format that needs to be unpacked to create the edit buffer.

(cherry picked from commit 7a51513)
  • Loading branch information
christofmuc committed Jan 15, 2021
1 parent 71e522b commit ce89b9c
Showing 1 changed file with 210 additions and 0 deletions.
210 changes: 210 additions & 0 deletions adaptions/YamahaDX7.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
#
# Copyright (c) 2021 Christof Ruch. All rights reserved.
#
# Dual licensed: Distributed under Affero GPL license by default, an MIT license is available for purchase
#
from ctypes import *


class OPERATOR_PACKED(Structure):
_pack_ = 1
_fields_ = [
("first_section", c_uint8 * 11),
("scale_left_curve", c_uint8, 2),
("scale_right_curve", c_uint8, 2),
("unused1", c_uint8, 4),
("rate_scale", c_uint8, 3),
("detune", c_uint8, 4),
("unused2", c_uint8, 1),
("amp_mod_sensivity", c_uint8, 2),
("key_vel_sensivity", c_uint8, 3),
("unused3", c_uint8, 3),
("output_level", c_uint8),
("osc_mode", c_uint8, 1),
("freq_coarse", c_uint8, 5),
("unused4", c_uint8, 2),
("freq_fine", c_uint8),
]


class VOICE_PACKED(Structure):
_pack_ = 1
_fields_ = [
("operators", OPERATOR_PACKED * 6),
("envs", c_uint8 * 8),
("algorithm", c_uint8, 5),
("unused1", c_uint8, 3),
("feedback", c_uint8, 3),
("osc_key_sync", c_uint8, 1),
("unused1", c_uint8, 4),
("lfo", c_uint8 * 4),
("lfo_sync", c_uint8, 1),
("lfo_wave", c_uint8, 3),
("lfo_pitch_mod_sens", c_uint8, 4),
("transpose", c_uint8),
("name", c_uint8 * 10)
]


def name():
return "Yamaha DX7"


def createDeviceDetectMessage(channel):
# The DX7 cannot be auto detected, because it doesn't reply to any message sent. Just return an empty MIDI message
return [0xf0, 0xf7]


def needsChannelSpecificDetection():
return False


def deviceDetectWaitMilliseconds():
# If this value is negative, the KnobKraft Orm will skip the auto-detection altogether
return -1


def channelIfValidDeviceResponse(message):
# The DX7 cannot be auto detected, because it doesn't reply to any message sent
return -1


def numberOfBanks():
return 1


def numberOfPatchesPerBank():
return 32


def createEditBufferRequest(channel):
# Can the DX7 actually do that? I don't think so.
return [0xf0, 0xf7]


def isEditBufferDump(message):
# It's not really edit buffer dumps, but single voice dumps
return len(message) > 5 and message[:6] == [0xf0, 0x43, 0x00, 0x00, 0x01, 0x1b]


def convertToEditBuffer(channel, message):
if isEditBufferDump(message):
return message
raise Exception("This is not an EditBufferDump")


def createBankDumpRequest(channel, bank):
# A bank is a single sysex message containing 32 voices (patches). Ask for it.
return [0xf0, 0x43, 0x20, 0x09, 0xf7]


def isPartOfBankDump(message):
return len(message) > 5 and message[:6] == [0xf0, 0x43, 0x00, 0x09, 0x20, 0x00]


def isBankDumpFinished(messages):
# We only need a single bank dump message
return any([isPartOfBankDump(m) for m in messages])


def extractPatchesFromBank(message):
patches = []
if isPartOfBankDump(message):
data_block = message[6:-2]
# Check for hardcoded length of bank dump
if len(data_block) == (0x20 << 7 | 0x00):
if checksum(data_block) == message[-2]:
# Checksum correct
for i in range(32):
voice = packedVoiceToSingleVoice(data_block[i * 128: (i + 1) * 128])
patches += singlePatchFromVoice(voice)
return patches
print("Checksum error encountered in DX7 bulk dump")
return []
print("Got DX7 bulk dump of invalid length - data length is %d but was expected to be %d" % (
len(data_block), (0x20 << 7)))
return []


def nameFromDump(message):
if isEditBufferDump(
message):
data_block = message[6:-2]
if len(data_block) == 155:
# The last 10 bytes of the data block of the common message are the name
return "".join([chr(x) for x in data_block[-10:]])
raise Exception("Can only extract a name from a single program dump")


def setupHelp():
return "The Yamaha DX7 is an early synth, it's MIDI implementation is really simple.\n" \
"Sorry to say that you cannot request the edit buffer or a bank dump from the computer, you need" \
" to use the receive manual dump function and initiate the dumps from the DX7.\n\n" \
"To send a patch to the DX7 for audition, make sure the INTERNAL MEMORY PROTECT is set to off."


def packedVoiceToSingleVoice(packed):
# This is the ugly function that needs to unpack 155 bytes from the 128 byte packed data format
source = VOICE_PACKED.from_buffer_copy(bytearray(packed))
dest = []
for op in source.operators:
dest += op.first_section
dest.append(op.scale_left_curve)
dest.append(op.scale_right_curve)
dest.append(op.rate_scale)
dest.append(op.amp_mod_sensivity)
dest.append(op.key_vel_sensivity)
dest.append(op.output_level)
dest.append(op.osc_mode)
dest.append(op.freq_coarse)
dest.append(op.freq_fine)
dest.append(op.detune)
dest += source.envs
dest.append(source.algorithm)
dest.append(source.feedback)
dest.append(source.osc_key_sync)
dest += source.lfo
dest.append(source.lfo_sync)
dest.append(source.lfo_wave)
dest.append(source.lfo_pitch_mod_sens)
dest.append(source.transpose)
dest += source.name
return dest


def singlePatchFromVoice(voice):
return [0xf0, 0x43, 0x00, 0x00, 0x01, 0x1b] + voice + [checksum(voice), 0xf7]


def checksum(data_block):
check_sum = 0
for d in data_block:
check_sum -= d
return check_sum & 0x7f


def splitSysexMessage(messages):
result = []
start = 0
read = 0
while read < len(messages):
if messages[read] == 0xf0:
start = read
elif messages[read] == 0xf7:
result.append(messages[start:read + 1])
read = read + 1
return result


def run_tests():
with open(R"testData/yamahaDX7-ROM2B.SYX", "rb") as sysex:
data = list(sysex.read())
assert isPartOfBankDump(data)
patches = splitSysexMessage(extractPatchesFromBank(data))
assert len(patches) == 32
for p in patches:
print(nameFromDump(p))


if __name__ == "__main__":
run_tests()

0 comments on commit ce89b9c

Please sign in to comment.