Added 4klang binary format construct.

This commit is contained in:
Alexander Kraus 2022-08-27 20:06:16 +02:00
parent 7eb473e67e
commit f58adab782
4 changed files with 368 additions and 0 deletions

1
4kp-convert/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
__pycache__

0
4kp-convert/__init__.py Normal file
View File

356
4kp-convert/file_format.py Normal file
View File

@ -0,0 +1,356 @@
import typing
from construct import Struct, Array, Bytes, Int32ul, Enum, Int8ul, Const, Switch, this, Error, Padded, FlagsEnum, PaddedString, Container, ListContainer, EnumIntegerString, EnumInteger
from enum import IntEnum, IntFlag
MAX_POLYPHONY = 2
MAX_INSTRUMENTS = 16
MAX_UNITS = 64
MAX_UNIT_SLOTS = 16
MAX_INSTRUMENT_NAME_LENGTH = 64
DEFAULT_GLOBAL_NAME = b'GlobalUnitsStoredAs.4ki '
class VersionTag(IntEnum or IntFlag):
VERSION_TAG_10 = 0x30316b34 # 4k10
VERSION_TAG_11 = 0x31316b34 # 4k11
VERSION_TAG_12 = 0x32316b34 # 4k12
VERSION_TAG_13 = 0x33316b34 # 4k13
VERSION_TAG_CURRENT = 0x34316b34 # 4k14
class UnitId(IntEnum or IntFlag):
M_NONE = 0x0
M_ENV = 0x1
M_VCO = 0x2
M_VCF = 0x3
M_DST = 0x4
M_DLL = 0x5
M_FOP = 0x6
M_FST = 0x7
M_PAN = 0x8
M_OUT = 0x9
M_ACC = 0xA
M_FLD = 0xB
M_GLITCH = 0xC
NUM_MODULES = 0xD
class VCOFlags(IntEnum or IntFlag):
VCO_SINE = 0x01
VCO_TRISAW = 0x02
VCO_PULSE = 0x04
VCO_NOISE = 0x08
VCO_LFO = 0x10
VCO_GATE = 0x20
VCO_STEREO = 0x40
class VCFType(IntEnum or IntFlag):
VCF_LOWPASS = 0x1
VCF_HIGHPASS = 0x2
VCF_BANDPASS = 0x4
VCF_BANDSTOP = 0x3
VCF_ALLPASS = 0x7
VCF_PEAK = 0x8
VCF_STEREO = 0x10
class FOPFlags(IntEnum or IntFlag):
FOP_POP = 0x1
FOP_ADDP = 0x2
FOP_MULP = 0x3
FOP_PUSH = 0x4
FOP_XCH = 0x5
FOP_ADD = 0x6
FOP_MUL = 0x7
FOP_ADDP2 = 0x8
FOP_LOADNOTE = 0x9
FOP_MULP2 = 0xA
class FSTType(IntEnum or IntFlag):
FST_SET = 0x00
FST_ADD = 0x10
FST_MUL = 0x20
FST_POP = 0x40
class ACCFlags(IntEnum or IntFlag):
ACC_OUT = 0x0
ACC_AUX = 0x8
formatnone = Bytes(MAX_UNIT_SLOTS - 1)
formatenv = Struct(
"attack" / Int8ul,
"decay" / Int8ul,
"sustain" / Int8ul,
"release" / Int8ul,
"gain" / Int8ul,
)
formatvco = Struct(
"transpose" / Int8ul,
"detune" / Int8ul,
"phhaseofs" / Int8ul,
"gate" / Int8ul,
"color" / Int8ul,
"shape" / Int8ul,
"gain" / Int8ul,
"flags" / FlagsEnum(Int8ul, VCOFlags),
)
# TODO: how do we add this conveniently?
formatvco11 = Struct(
"transpose" / Int8ul,
"detune" / Int8ul,
"phhaseofs" / Int8ul,
"color" / Int8ul,
"shape" / Int8ul,
"gain" / Int8ul,
"flags" / FlagsEnum(Int8ul, VCOFlags),
)
formatvcf = Struct(
"freq" / Int8ul,
"res" / Int8ul,
"type" / Enum(Int8ul, VCFType),
)
formatdst = Struct(
"drive" / Int8ul,
"snhfreq" / Int8ul,
"stereo" / Int8ul,
)
formatdll = Struct(
"pregain" / Int8ul,
"dry" / Int8ul,
"feedback" / Int8ul,
"damp" / Int8ul,
"freq" / Int8ul,
"depth" / Int8ul,
"delay" / Int8ul,
"count" / Int8ul,
"guidelay" / Int8ul,
"synctype" / Int8ul, # TODO: Enum? Where?
"leftreverb" / Int8ul,
"reverb" / Int8ul,
)
# TODO: Implement the migrations
formatdll10 = Struct(
"pregain" / Int8ul,
"dry" / Int8ul,
"feedback" / Int8ul,
"damp" / Int8ul,
"delay" / Int8ul,
"count" / Int8ul,
"guidelay" / Int8ul,
"synctype" / Int8ul, # TODO: Enum? Where?
"leftreverb" / Int8ul,
"reverb" / Int8ul,
)
formatfop = Struct(
"flags"/ FlagsEnum(Int8ul, FOPFlags),
)
formatfst = Struct(
"amount" / Int8ul,
"type" / Enum(Int8ul, FSTType),
"dest_stack" / Int8ul,
"dest_unit" / Int8ul,
"dest_slot" / Int8ul,
"dest_id" / Int8ul,
)
formatpan = Struct(
"panning" / Int8ul,
)
formatout = Struct(
"gain" / Int8ul,
"auxsend" / Int8ul,
)
formatacc = Struct(
"flags" / FlagsEnum(Int8ul, ACCFlags),
)
formatfld = Struct(
"value" / Int8ul,
)
formatglitch = Struct(
"active" / Int8ul,
"dry" / Int8ul,
"dsize" / Int8ul,
"dpitch" / Int8ul,
"delay" / Int8ul,
"guidelay" / Int8ul,
)
# TODO: unclear what this does, find out.
formatnummodules = Struct(
"placeholder" / Int8ul,
)
format4ku = Struct(
"id" / Enum(Int8ul, UnitId),
"slots" / Padded(
MAX_UNIT_SLOTS - 1,
Switch(
keyfunc = this.id,
cases = {
UnitId.M_ENV.name: formatenv,
UnitId.M_VCO.name: formatvco,
UnitId.M_VCF.name: formatvcf,
UnitId.M_DST.name: formatdst,
UnitId.M_DLL.name: formatdll,
UnitId.M_FOP.name: formatfop,
UnitId.M_FST.name: formatfst,
UnitId.M_PAN.name: formatpan,
UnitId.M_OUT.name: formatout,
UnitId.M_ACC.name: formatacc,
UnitId.M_FLD.name: formatfld,
UnitId.M_GLITCH.name: formatglitch,
UnitId.NUM_MODULES.name: formatnummodules,
UnitId.M_NONE.name: formatnone,
},
default = formatnone,
),
),
)
format4ki = Struct(
"versionTag" / Enum(Int32ul, VersionTag),
"instrumentName" / PaddedString(MAX_INSTRUMENT_NAME_LENGTH, 'utf-8'),
"units" / Array(MAX_UNITS, format4ku),
)
format4kp = Struct(
"versionTag" / Enum(Int32ul, VersionTag),
"polyphony" / Int32ul,
"instrumentNames" / Array(MAX_INSTRUMENTS, PaddedString(MAX_INSTRUMENT_NAME_LENGTH, 'utf-8')),
"instrumentValues" / Array(MAX_INSTRUMENTS * MAX_UNITS, format4ku),
"globalValues" / Array(MAX_UNITS, format4ku),
)
class FormatConverter:
builtins = ['copy', 'search', 'search_all', 'update']
@staticmethod
def ConstructToDictTrivial(construct: typing.Any) -> dict:
'''
Convert a construct container to a dictionary for stupid-simple
json and yaml dumps. Those do not work with sointu, as its json
and yaml formats are simpler and more structured than 4klang's
binary format.
Parameters:
construct (typing.Any): Construct to convert to dictionary.
Returns:
result (dict): json-serializable dictionary.
'''
if type(construct) is Container:
result = {}
children = list(filter(lambda id: not id.startswith('_') and not id in FormatConverter.builtins, dir(construct)))
for child in children:
result[child] = FormatConverter.ConstructToDict(getattr(construct, child))
return result
elif type(construct) is ListContainer:
result = []
for child in construct:
result.append(FormatConverter.ConstructToDict(child))
return result
elif type(construct) is EnumIntegerString:
return str(construct)
elif type(construct) is EnumInteger:
return int(construct)
elif type(construct) is int:
return int(construct)
elif type(construct) is bytes:
return construct.decode('utf-8')
elif type(construct) is bool:
return construct
elif type(construct) is str:
return str(construct).split('\x00')[0]
raise Exception("Unrecognized construct type: {}".format(type(construct)))
# TODO: It needs to be verified whether those identifiers
# are the correct ones for sointu.
@staticmethod
def UnitTypeName(id: UnitId) -> str:
if id == UnitId.M_ACC.name:
return "accumulate"
elif id == UnitId.M_DLL.name:
return "delay"
elif id == UnitId.M_DST.name:
return "distort"
elif id == UnitId.M_ENV.name:
return "envelope"
elif id == UnitId.M_FLD.name:
return "load"
elif id == UnitId.M_FOP.name:
# TODO: depending on the arithmetic handling in sointu,
# this might not be the correct approach
return "arithmetic"
elif id == UnitId.M_FST.name:
return "store"
elif id == UnitId.M_GLITCH.name:
return "glitch"
elif id == UnitId.M_OUT.name:
return "out"
elif id == UnitId.M_PAN.name:
return "pan"
elif id == UnitId.M_VCF.name:
return "filter"
elif id == UnitId.M_VCO.name:
return "oscillator"
elif id == UnitId.M_NONE.name:
return "none"
else:
return "unsupported"
@staticmethod
def UnitContainerToDict(construct: typing.Any) -> dict:
unitType = FormatConverter.UnitTypeName(construct.id)
slots = {}
for (name, value) in construct.slots.items():
if name == '_io':
continue
slots[name] = value
return {
"type": unitType,
"parameters": slots,
}
@staticmethod
def PatchContainerToDict(construct: typing.Any) -> dict:
instruments = []
for instrumentIndex in range(MAX_INSTRUMENTS):
instrument = []
for unitIndex in range(MAX_UNIT_SLOTS):
valueIndex = instrumentIndex*MAX_UNIT_SLOTS + unitIndex
# Skip empty units.
if construct.instrumentValues[valueIndex].id == UnitId.M_NONE.name:
continue
unitDict = FormatConverter.UnitContainerToDict(construct.instrumentValues[valueIndex])
instrument.append(unitDict)
if instrument != []:
instruments.append({
"numvoices": 1,
"units": instrument,
})
return {
"patch": instruments
}
@staticmethod
def migrate(fromVersion: VersionTag, toVersion: VersionTag) -> typing.Any:
return None

11
4kp-convert/tool.py Normal file
View File

@ -0,0 +1,11 @@
import json
import sys
import yaml
from textwrap import indent
from file_format import *
if __name__ == '__main__':
parsedFileContent = format4kp.parse_file(sys.argv[1])
sointuFormat = FormatConverter.PatchContainerToDict(parsedFileContent)
print(yaml.dump(sointuFormat, indent=4))