From f58adab782c3724f5e2a972db1b915dc32de66d0 Mon Sep 17 00:00:00 2001 From: Alexander Kraus Date: Sat, 27 Aug 2022 20:06:16 +0200 Subject: [PATCH] Added 4klang binary format construct. --- 4kp-convert/.gitignore | 1 + 4kp-convert/__init__.py | 0 4kp-convert/file_format.py | 356 +++++++++++++++++++++++++++++++++++++ 4kp-convert/tool.py | 11 ++ 4 files changed, 368 insertions(+) create mode 100644 4kp-convert/.gitignore create mode 100644 4kp-convert/__init__.py create mode 100644 4kp-convert/file_format.py create mode 100644 4kp-convert/tool.py diff --git a/4kp-convert/.gitignore b/4kp-convert/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/4kp-convert/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/4kp-convert/__init__.py b/4kp-convert/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/4kp-convert/file_format.py b/4kp-convert/file_format.py new file mode 100644 index 0000000..b0de944 --- /dev/null +++ b/4kp-convert/file_format.py @@ -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 \ No newline at end of file diff --git a/4kp-convert/tool.py b/4kp-convert/tool.py new file mode 100644 index 0000000..f230785 --- /dev/null +++ b/4kp-convert/tool.py @@ -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))