Skip to content

Commit

Permalink
Looking at the Emu Proteus family they mostly differ by the memory la…
Browse files Browse the repository at this point in the history
…yout. Starting a generic E-mu module here, but I do not really want to have to create one file per Synth, they have so many!
  • Loading branch information
christofmuc committed Feb 1, 2025
1 parent 1a95a8b commit b13f778
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 121 deletions.
116 changes: 0 additions & 116 deletions adaptations/Emu_Proteus.py

This file was deleted.

28 changes: 28 additions & 0 deletions adaptations/Emu_Proteus1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import sys
from typing import List

import knobkraft
import testing
from emu.GenericEmu import GenericEmu

this_module = sys.modules[__name__]

proteus1 = GenericEmu(name="E-mu Proteus/1", model_id=0x04, banks= [
{"bank": 0, "name": "ROM", "size": 64, "type": "Preset", "isROM": True},
{"bank": 1, "name": "RAM", "size": 64, "type": "Preset"},
{"bank": 2, "name": "CARD", "size": 64, "type": "Preset", "isRom": True}
])
proteus1.install(this_module)


# Test data picked up by test_adaptation.py
def make_test_data():
def programs(data: testing.TestData) -> List[testing.ProgramTestData]:
yield testing.ProgramTestData(message=data.all_messages[0], name="FMstylePiano", number=64) # Adjusted test data for Proteus
yield testing.ProgramTestData(message=data.all_messages[63], name="BarberPole ", number=127)

additional_syx = "testData/E-mu_Proteus/Proteus2Presets.syx"
more_messages = knobkraft.load_sysex(additional_syx, as_single_list=False)
yield testing.ProgramTestData(message=more_messages[0], name="Winter Signs", number=64) # Adjusted test data for Proteus

return testing.TestData(sysex="testData/E-mu_Proteus/Proteus1Presets.syx", program_generator=programs)
32 changes: 32 additions & 0 deletions adaptations/Emu_Proteus1XR.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import sys
from typing import List

import knobkraft
import testing
from emu.GenericEmu import GenericEmu

this_module = sys.modules[__name__]

proteus1xr = GenericEmu(name="E-mu Proteus/1 XR", model_id=0x07, banks=[
{"bank": 0, "name": "RAM", "size": 64, "type": "Preset"},
{"bank": 1, "name": "RAM", "size": 64, "type": "Preset"},
{"bank": 2, "name": "RAM", "size": 64, "type": "Preset"},
{"bank": 3, "name": "RAM", "size": 64, "type": "Preset"},
{"bank": 4, "name": "ROM", "size": 64, "type": "Preset", "isRom": True},
{"bank": 5, "name": "ROM", "size": 64, "type": "Preset", "isRom": True}
])
proteus1xr.install(this_module)


# Test data picked up by test_adaptation.py
def make_test_data():
def programs(data: testing.TestData) -> List[testing.ProgramTestData]:
yield testing.ProgramTestData(message=data.all_messages[0], name="Stereo Piano", number=0) # Adjusted test data for Proteus
yield testing.ProgramTestData(message=data.all_messages[63], name="Mtlphn Arp 9", number=63)
yield testing.ProgramTestData(message=data.all_messages[191], name="Lazer Ray ", number=191)

additional_syx = "testData/E-mu_Proteus/Proteus2Presets.syx"
more_messages = knobkraft.load_sysex(additional_syx, as_single_list=False)
yield testing.ProgramTestData(message=more_messages[0], name="Winter Signs", number=64) # Adjusted test data for Proteus

return testing.TestData(sysex="testData/E-mu_Proteus/Proteus1XRPresets.syx", program_generator=programs)
114 changes: 114 additions & 0 deletions adaptations/emu/GenericEmu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import hashlib
from typing import List, Dict

from knobkraft import knobkraft_api

EMU_ID = 0x18
PROTEUS_IDS = {
0x04: "Proteus 1", 0x05: "Proteus 2", 0x06: "Proteus 3",
0x07: "Proteus 1 XR", 0x08: "Proteus 2 XR", 0x09: "Proteus 3 XR",
0x0A: "1+Orchestral"
}
COMMAND_PRESET_REQUEST = 0x00
COMMAND_PRESET_DATA = 0x01
COMMAND_VERSION_REQUEST = 0x0a
COMMAND_VERSION_DATA = 0x0b


class GenericEmu:

def __init__(self, name: str, model_id: int, banks: List[Dict]):
self._name = name
self._model_id = model_id
self._banks = banks

@knobkraft_api
def name(self):
return self._name

@knobkraft_api
def createDeviceDetectMessage(self, device_id):
return [0xf0, EMU_ID, self._model_id, device_id & 0x0f, COMMAND_VERSION_REQUEST, 0xf7]

@knobkraft_api
def needsChannelSpecificDetection(self):
return True

@knobkraft_api
def deviceDetectWaitMilliseconds(self):
return 300

@knobkraft_api
def channelIfValidDeviceResponse(self, message):
if (len(message) > 5
and message[0] == 0xf0
and message[1] == EMU_ID
and message[4] == COMMAND_VERSION_DATA):
if message[2] in PROTEUS_IDS:
return message[3] # Device ID is at index 3
return -1

@knobkraft_api
def bankDescriptors(self):
return self._banks

@knobkraft_api
def createProgramDumpRequest(self, device_id, patchNo):
if not 0 <= patchNo < 320:
raise ValueError(f"Patch number out of range: {patchNo} should be from 0 to 319")
return [[0xf0, EMU_ID, model_id, device_id & 0x0f, COMMAND_PRESET_REQUEST, patchNo & 0x7f, (patchNo >> 7) & 0x7f, 0xf7] for model_id in PROTEUS_IDS]

@knobkraft_api
def isSingleProgramDump(self, message: List[int]) -> bool:
return (len(message) > 5
and message[0] == 0xf0
and message[1] == EMU_ID
and message[2] in PROTEUS_IDS
and message[4] == COMMAND_PRESET_DATA)

@knobkraft_api
def nameFromDump(self, message: List[int]) -> str:
if self.isSingleProgramDump(message):
offset = 7
name_chars = []
for _ in range(12):
val = message[offset] + (message[offset + 1] << 7)
name_chars.append(chr(val))
offset += 2
return ''.join(name_chars)
return 'invalid'

@knobkraft_api
def numberFromDump(self, message: List[int]) -> int:
if self.isSingleProgramDump(message):
return message[5] + (message[6] << 7)
return -1

@knobkraft_api
def convertToProgramDump(self, device_id, message, program_number):
if self.isSingleProgramDump(message):
return message[0:3] + [device_id & 0x0f] + message[4:5] + [program_number & 0x7f, (program_number >> 7) & 0x7f] + message[7:]
raise Exception("Can only convert program dumps")

@knobkraft_api
def renamePatch(self, message: List[int], new_name: str) -> List[int]:
if self.isSingleProgramDump(message):
name_params = [(ord(c) & 0x7f, (ord(c) >> 7) & 0x7f) for c in new_name.ljust(12, " ")]
return message[:7] + [item for sublist in name_params for item in sublist] + message[31:]
raise Exception("Can only rename Presets!")

@knobkraft_api
def calculateFingerprint(self, message: List[int]):
if self.isSingleProgramDump(message):
data = message[8:-1]
data[0:24] = [0] * 24
return hashlib.md5(bytearray(data)).hexdigest()
raise Exception("Can only fingerprint Presets")

def install(self, module):
# This is required because the original KnobKraft modules are not objects, but rather a module namespace with
# methods declared. Expose our objects methods in the top level module namespace so the C++ code finds it
for a in dir(self):
if callable(getattr(self, a)) and hasattr(getattr(self, a), "_is_knobkraft"):
# this was helpful: http://stupidpythonideas.blogspot.com/2013/06/how-methods-work.html
setattr(module, a, getattr(self, a))
5 changes: 5 additions & 0 deletions adaptations/emu/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#
# Copyright (c) 2025 Christof Ruch. All rights reserved.
#
# Dual licensed: Distributed under Affero GPL license by default, an MIT license is available for purchase
#
6 changes: 6 additions & 0 deletions adaptations/knobkraft/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@
#
from .sysex import *
from .test_helper import *


def knobkraft_api(func):
func._is_knobkraft = True
return func

6 changes: 1 addition & 5 deletions adaptations/roland/GenericRoland.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import hashlib
from typing import List, Tuple, Optional, Dict
import knobkraft
from knobkraft import knobkraft_api

roland_id = 0x41 # Roland
command_rq1 = 0x11
Expand Down Expand Up @@ -166,11 +167,6 @@ def address_and_size_for_all_request(self, sub_address) -> Tuple[List[int], List
return concrete_address, self.total_size_as_list()


def knobkraft_api(func):
func._is_knobkraft = True
return func


class GenericRoland:
def __init__(self, name: str, model_id: List[int], address_size: int, edit_buffer: RolandData, program_dump: RolandData,
category_index: Optional[int] = None,
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 comments on commit b13f778

Please sign in to comment.