From 907a93e8b972ea1f104e2728f97a9721428c1c7c Mon Sep 17 00:00:00 2001 From: =?utf8?q?Michal=20Mal=C3=BD?= Date: Tue, 17 Jun 2014 16:13:25 +0200 Subject: [PATCH] Initial commit. --- ffe_parse.py | 548 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 548 insertions(+) create mode 100644 ffe_parse.py diff --git a/ffe_parse.py b/ffe_parse.py new file mode 100644 index 0000000..4f7613e --- /dev/null +++ b/ffe_parse.py @@ -0,0 +1,548 @@ +# Script to parse the FFE files +# +# This script requires 'Construct' to run: +# https://pypi.python.org/pypi/construct + +from construct import * +import argparse + +try: + import os + import sys + import ctypes + import sdl2 + _hasPySDL2 = True +except ImportError: + _hasPySDL2 = False + +guid_constant = ' \x1cT\x133\x8e\xd0\x11\x9a\xd0\x00\xa0\xc9\xa0n5' +guid_ramp = '!\x1cT\x133\x8e\xd0\x11\x9a\xd0\x00\xa0\xc9\xa0n5' +guid_square = '"\x1cT\x133\x8e\xd0\x11\x9a\xd0\x00\xa0\xc9\xa0n5' +guid_sine = '#\x1cT\x133\x8e\xd0\x11\x9a\xd0\x00\xa0\xc9\xa0n5' +guid_triangle = '$\x1cT\x133\x8e\xd0\x11\x9a\xd0\x00\xa0\xc9\xa0n5' +guid_sawup = '%\x1cT\x133\x8e\xd0\x11\x9a\xd0\x00\xa0\xc9\xa0n5' +guid_sawdown = '&\x1cT\x133\x8e\xd0\x11\x9a\xd0\x00\xa0\xc9\xa0n5' +guid_spring = "'\x1cT\x133\x8e\xd0\x11\x9a\xd0\x00\xa0\xc9\xa0n5" +guid_damper = '(\x1cT\x133\x8e\xd0\x11\x9a\xd0\x00\xa0\xc9\xa0n5' +guid_inertia = ')\x1cT\x133\x8e\xd0\x11\x9a\xd0\x00\xa0\xc9\xa0n5' +guid_friction = '*\x1cT\x133\x8e\xd0\x11\x9a\xd0\x00\xa0\xc9\xa0n5' +guid_custom = '+\x1cT\x133\x8e\xd0\x11\x9a\xd0\x00\xa0\xc9\xa0n5' + +#--- + +header = Struct("header", + Const(Bytes("magic", 4), "RIFF"), + UBInt32("length"), + + Const(Bytes("magic2", 4), "FORC"), + Bytes("header", 16), + + OptionalGreedyRange(LazyBound("listBlock", lambda: listBlock)), +) + +#--- + +listBlock = Struct("listBlock", + Const(Bytes("magic", 4), "LIST"), + ULInt32("length"), + + LazyBound("effectBlock", lambda: effectBlock), + + Bytes("Guid", 16), + ULInt32("lpDiEffect"), # name is guessed + + LazyBound("dataAxis", lambda: dataAxis), + LazyBound("dataDirection", lambda: dataDirection ), + + If(this.effectBlock.lpEnvelope, + LazyBound("dataEnvelope", lambda: dataEnvelope), + ), + + # Is using Guid the only way? + Switch("dataSpecific" , this.Guid, + { + guid_constant : LazyBound("dataSpecificConstant", lambda: dataSpecificConstant), + guid_ramp : LazyBound("dataSpecificRamp", lambda: dataSpecificRamp), + guid_square : LazyBound("dataSpecificPeriodic", lambda: dataSpecificPeriodic), + guid_sine : LazyBound("dataSpecificPeriodic", lambda: dataSpecificPeriodic), + guid_triangle : LazyBound("dataSpecificPeriodic", lambda: dataSpecificPeriodic), + guid_sawup : LazyBound("dataSpecificPeriodic", lambda: dataSpecificPeriodic), + guid_sawdown : LazyBound("dataSpecificPeriodic", lambda: dataSpecificPeriodic), + guid_spring : LazyBound("dataSpecificSpring", lambda: dataSpecificSpring), + guid_damper : LazyBound("dataSpecificSpring", lambda: dataSpecificSpring), + guid_inertia : LazyBound("dataSpecificSpring", lambda: dataSpecificSpring), + guid_friction : LazyBound("dataSpecificSpring", lambda: dataSpecificSpring), + guid_custom : LazyBound("dataSpecificCustom", lambda: dataSpecificCustom), + }, + default = Pass + ), +) + +effectBlock = Struct("effectBlock", + Const(Bytes("magic", 4), "efct"), + + CString("szFriendlyName"), + Padding(lambda ctx: 63 - len(ctx["szFriendlyName"])), # pad name to fill 64 bytes + + # Reference: + # http://msdn.microsoft.com/en-us/library/windows/desktop/microsoft.directx_sdk.reference.dieffect%28v=vs.85%29.aspx + + ULInt32("dwSize"), + + BitStruct("dwFlags", # Low Byte MSB's first + Padding(1), + BitField("DIEFF_SPHERICAL", 1), + BitField("DIEFF_POLAR", 1), + BitField("DIEFF_CARTESIAN", 1), + Padding(2), + BitField("DIEFF_OBJECTOFFSETS", 1), + BitField("DIEFF_OBJECTIDS", 1), + Padding(24), + ), + + ULInt32("dwDuration"), + ULInt32("dwSamplePeriod"), + ULInt32("dwGain"), + ULInt32("dwTriggerButton"), + ULInt32("dwTriggerRepeatInterval"), + ULInt32("cAxes"), + ULInt32("rgdwAxes"), + ULInt32("rglDirection"), + ULInt32("lpEnvelope"), + ULInt32("cbTypeSpecificParams"), + ULInt32("lpvTypeSepecificParams"), + ULInt32("dwStartDelay"), +) + +#--- + +dataAxis = Struct("dataAxis", + Const(Bytes("magic", 4), "data"), + ULInt32("length"), + + Array(this._.effectBlock.cAxes, ULInt32("axis")), # 'this._.' refers to parent +) + +dataDirection = Struct("dataDirection", + Const(Bytes("magic", 4), "data"), + ULInt32("length"), + + Array(this._.effectBlock.cAxes, SLInt32("direction")), +) + +dataEnvelope = Struct("dataEnvelope", + Const(Bytes("magic", 4), "data"), + ULInt32("length"), + + ULInt32("dwSize"), + ULInt32("dwAttackLevel"), + ULInt32("dwAttackTime"), + ULInt32("dwFadeLevel"), + ULInt32("dwFadeTime"), +) + +#--- + +dataSpecificConstant = Struct("dataSpecificConstant", + Const(Bytes("magic", 4), "data"), + ULInt32("length"), + + SLInt16("lMagnitude"), +) + +dataSpecificRamp = Struct("dataSpecificRamp", + Const(Bytes("magic", 4), "data"), + ULInt32("length"), + + SLInt32("lStart"), + SLInt32("lEnd"), +) + +dataSpecificPeriodic = Struct("dataSpecificPeriodic", + Const(Bytes("magic", 4), "data"), + ULInt32("length"), + + ULInt32("dwMagnitude"), + SLInt32("lOffset"), + ULInt32("dwPhase"), + ULInt32("dwPeriod"), +) + +dataSpecificSpring = Struct("dataSpecificSpring", + Const(Bytes("magic", 4), "data"), + ULInt32("length"), + + Array(this._.effectBlock.cAxes, Struct("dataSpecificSpringArray", + SLInt32("lOffset"), + SLInt32("lPositiveCoefficient"), + SLInt32("lNegativeCoefficient"), + ULInt32("dwPositiveSaturation"), + ULInt32("dwNegativeSaturation"), + SLInt32("lDeadBand"), + ), + ), +) + +dataSpecificCustom = Struct("dataSpecificCustom", + Const(Bytes("magic", 4), "data"), + ULInt32("length"), + + ULInt32("cChannels"), + ULInt32("dwSamplePeriod"), + ULInt32("cSamples"), + + Array(this.cSamples, Array(this.cChannels, ULInt16("rglForceData"))), +) + +#--- +# check length of dataAxis/etc - redefine as block refers to higher section +# new block includes the cAxis count, so overall length is increased by 4 bytes + +dataAxisLen = Struct("dataAxisLen", + Struct("effectBlock", ULInt32("cAxes"),), + LazyBound("dataAxis", lambda: dataAxis), +) + +dataDirectionLen = Struct("dataDirectionLen", + Struct("effectBlock", ULInt32("cAxes"),), + LazyBound("dataDirection", lambda: dataDirection), +) + +dataSpecificSpringLen = Struct("dataSpecificSpingLen", + Struct("effectBlock", ULInt32("cAxes"),), + LazyBound("dataSpecific", lambda: dataSpecificSpring), +) + +def fix_block_lengths(effect): + effect.length = len(listBlock.build(effect)) - 8 + + # fix effectBlock - does this ever change? + effect.effectBlock.length = len(effectBlock.build(effect.effectBlock)) - 68 # name is not included in size + + # fix Axis + effect.dataAxis.length = len(dataAxisLen.build(effect)) - 12 + + # fix Direction + effect.dataDirection.length = len(dataDirectionLen.build(effect)) - 12 + + if (effect.effectBlock.lpEnvelope): + effect.dataEnvelope.length = len(dataEnvelope.build(effect.dataEnvelope)) - 8 + + # fix data specific blocks + if (effect.Guid == guid_constant): + effect.dataSpecific.length = len(dataSpecificConstant.build(effect.dataSpecific)) - 8 + + if (effect.Guid == guid_ramp): + effect.dataSpecific.length = len(dataSpecificRamp.build(effect.dataSpecific)) - 8 + + if (effect.Guid == guid_square or \ + effect.Guid == guid_sine or \ + effect.Guid == guid_triangle or \ + effect.Guid == guid_sawup or \ + effect.Guid == guid_sawdown): + effect.dataSpecific.length = len(dataSpecificPeriodic.build(effect.dataSpecific)) - 8 + + if (effect.Guid == guid_spring or \ + effect.Guid == guid_damper or \ + effect.Guid == guid_inertia or \ + effect.Guid == guid_friction): + effect.dataSpecific.length = len(dataSpecificSpringLen.build(effect)) - 12 + +#--- + +haptic = None + +def init_sdl2(device): + global haptic + if haptic != None: + return + + sdl2.SDL_Init(sdl2.SDL_INIT_TIMER | sdl2.SDL_INIT_JOYSTICK | sdl2.SDL_INIT_HAPTIC) + if (sdl2.SDL_NumHaptics() == 0): + print "Error: Unable to initilize SDL" + sdl2.SDL_Quit() + return + + for index in range(0,sdl2.SDL_NumHaptics()): + print "Found", index, ":", sdl2.SDL_HapticName(index) + + if device >= sdl2.SDL_NumHaptics(): + print "Error: Device not found" + sdl2.SDL_Quit() + return + + haptic = sdl2.SDL_HapticOpen(device); + if haptic == None: + print "Unable to open device" + sdl2.SDL_Quit() + return + +def play_effect(ffe, device=0, linux=0): + global haptic + if haptic == None: + init_sdl2(device) + + if haptic == None: + # fail out cleanly + return + + nefx = 0 + efx = [0] * 12 + id = [0] * 12 + stoptime = 0 + + supported = sdl2.SDL_HapticQuery(haptic) + + # assemble effects + for item in ffe.listBlock: + print "Effect", item.effectBlock.szFriendlyName + + if nefx==12: + print "Sorry too many effects" + break + + # Convert uSec -> mSec + timediv = 1000 + + # Overall Gain, SDL max force = +/-32767 + gain = 3.2767 * item.effectBlock.dwGain / 10000 + + # Envelope + if item.effectBlock.lpEnvelope: + attackLevel = int(item.dataEnvelope.dwAttackLevel * gain) + attackTime = item.dataEnvelope.dwAttackTime / timediv + fadeLevel = int(item.dataEnvelope.dwFadeLevel * gain) + fadeTime = item.dataEnvelope.dwFadeTime / timediv + else: + attackLevel = int(10000 * gain) + attackTime = 0 + fadeLevel = int(10000 * gain) + fadeTime = 0 + + # Direction + if sdl2.SDL_HapticNumAxes(haptic) == 1: + print " Warning: single axis device, forcing Cartesian mode" + direction=sdl2.SDL_HapticDirection(type=sdl2.SDL_HAPTIC_CARTESIAN, dir=(0,0,0)) + else: + direction=sdl2.SDL_HapticDirection(type=sdl2.SDL_HAPTIC_POLAR, dir=(9000,0,0)) + if item.effectBlock.dwFlags.DIEFF_CARTESIAN: + cart = item.dataDirection.direction[:2] + if linux: + cart[0] *= -1 + direction=sdl2.SDL_HapticDirection(type=sdl2.SDL_HAPTIC_CARTESIAN, dir=(tuple(cart))) + print "Cartesian", cart + + if item.effectBlock.dwFlags.DIEFF_POLAR: + polar = item.dataDirection.direction[:2] + direction=sdl2.SDL_HapticDirection(type=sdl2.SDL_HAPTIC_POLAR, dir=(tuple(polar))) + print "Polar", polar + + if item.effectBlock.dwFlags.DIEFF_SPHERICAL: + spherical = item.dataDirection.direction[:2] + direction=sdl2.SDL_HapticDirection(type=sdl2.SDL_HAPTIC_SPHERICAL, dir=(tuple(spherical))) + print "Spherical", spherical + + + if item.Guid == guid_constant and supported & sdl2.SDL_HAPTIC_CONSTANT: + print " Loading effect", nefx, "Constant Force" + efx[nefx] = sdl2.SDL_HapticEffect(type=sdl2.SDL_HAPTIC_CONSTANT, constant=sdl2.SDL_HapticConstant( \ + type=sdl2.SDL_HAPTIC_CONSTANT, \ + direction=direction, \ + level=int(item.dataSpecific.lMagnitude * gain), \ + length=item.effectBlock.dwDuration / timediv, \ + attack_length=attackTime, \ + attack_level=attackLevel, \ + fade_length=fadeTime, \ + fade_level=fadeLevel, \ + delay=item.effectBlock.dwStartDelay / timediv)) + id[nefx] = sdl2.SDL_HapticNewEffect(haptic, efx[nefx]) + if id[nefx] < 0: + print " Error", sdl2.SDL_GetError() + nefx += 1 + + if item.Guid == guid_square: + print " Warning: Square is not currently supported SDL2, using sine instead" + + if (item.Guid == guid_sine or item.Guid == guid_square) and supported & sdl2.SDL_HAPTIC_SINE: + print " Loading effect", nefx, "Sine Periodic" + efx[nefx] = sdl2.SDL_HapticEffect(type=sdl2.SDL_HAPTIC_SINE, periodic=sdl2.SDL_HapticPeriodic( \ + type=sdl2.SDL_HAPTIC_SINE, \ + direction=direction, \ + period=item.dataSpecific.dwPeriod / timediv, \ + magnitude=int(item.dataSpecific.dwMagnitude * gain), \ + offset=item.dataSpecific.lOffset, \ + phase=item.dataSpecific.dwPhase, \ + length=item.effectBlock.dwDuration, \ + attack_length=attackTime, \ + attack_level=attackLevel, \ + fade_length=fadeTime, \ + fade_level=fadeLevel, \ + delay=item.effectBlock.dwStartDelay / timediv)) + id[nefx] = sdl2.SDL_HapticNewEffect(haptic, efx[nefx]) + if id[nefx] < 0: + print " Error", sdl2.SDL_GetError() + nefx += 1 + + if item.Guid == guid_triangle and supported & sdl2.SDL_HAPTIC_TRIANGLE: + print " Loading effect", nefx, "Triangle Periodic" + efx[nefx] = sdl2.SDL_HapticEffect(type=sdl2.SDL_HAPTIC_TRIANGLE, periodic=sdl2.SDL_HapticPeriodic( \ + type=sdl2.SDL_HAPTIC_TRIANGLE, \ + direction=direction, \ + period=item.dataSpecific.dwPeriod / timediv, \ + magnitude=int(item.dataSpecific.dwMagnitude * gain), \ + offset=item.dataSpecific.lOffset, \ + phase=item.dataSpecific.dwPhase, \ + length=item.effectBlock.dwDuration / timediv, \ + attack_length=attackTime, \ + attack_level=attackLevel, \ + fade_length=fadeTime, \ + fade_level=fadeLevel, \ + delay=item.effectBlock.dwStartDelay / timediv)) + id[nefx] = sdl2.SDL_HapticNewEffect(haptic, efx[nefx]) + if id[nefx] < 0: + print " Error", sdl2.SDL_GetError() + nefx += 1 + + if item.Guid == guid_sawup and supported & sdl2.SDL_HAPTIC_SAWTOOTHUP: + print " Loading effect", nefx, "Sawtooth Up" + efx[nefx] = sdl2.SDL_HapticEffect(type=sdl2.SDL_HAPTIC_SAWTOOTHUP, periodic=sdl2.SDL_HapticPeriodic( \ + type=sdl2.SDL_HAPTIC_SAWTOOTHUP, \ + direction=direction, \ + period=item.dataSpecific.dwPeriod / timediv, \ + magnitude=int(item.dataSpecific.dwMagnitude * gain), \ + offset=item.dataSpecific.lOffset, \ + phase=item.dataSpecific.dwPhase, \ + length=item.effectBlock.dwDuration / timediv, \ + attack_length=attackTime, \ + attack_level=attackLevel, \ + fade_length=fadeTime, \ + fade_level=fadeLevel, \ + delay=item.effectBlock.dwStartDelay / timediv)) + id[nefx] = sdl2.SDL_HapticNewEffect(haptic, efx[nefx]) + if id[nefx] < 0: + print " Error", sdl2.SDL_GetError() + nefx += 1 + + if item.Guid == guid_sawdown and supported & sdl2.SDL_HAPTIC_SAWTOOTHDOWN: + print " Loading effect", nefx, "Sawtooth Down" + efx[nefx] = sdl2.SDL_HapticEffect(type=sdl2.SDL_HAPTIC_SAWTOOTHDOWN, periodic=sdl2.SDL_HapticPeriodic( \ + type=sdl2.SDL_HAPTIC_SAWTOOTHDOWN, \ + direction=direction, \ + period=item.dataSpecific.dwPeriod / timediv, \ + magnitude=int(item.dataSpecific.dwMagnitude * gain), \ + offset=item.dataSpecific.lOffset, \ + phase=item.dataSpecific.dwPhase, \ + length=item.effectBlock.dwDuration, \ + attack_length=attackTime, \ + attack_level=attackLevel, \ + fade_length=fadeTime, \ + fade_level=fadeLevel, \ + delay=item.effectBlock.dwStartDelay / timediv)) + id[nefx] = sdl2.SDL_HapticNewEffect(haptic, efx[nefx]) + if id[nefx] < 0: + print " Error", sdl2.SDL_GetError() + nefx += 1 + + if item.Guid == guid_ramp and supported & sdl2.SDL_HAPTIC_RAMP: + print " Loading effect", nefx, "Ramp" + efx[nefx] = sdl2.SDL_HapticEffect(type=sdl2.SDL_HAPTIC_RAMP, ramp=sdl2.SDL_HapticRamp( \ + type=sdl2.SDL_HAPTIC_RAMP, \ + direction=direction, \ + start=int(item.dataSpecific.lStart * gain), \ + end=int(item.dataSpecific.lEnd * gain), \ + length=item.effectBlock.dwDuration / timediv, \ + attack_length=attackTime, \ + attack_level=attackLevel, \ + fade_length=fadeTime, \ + fade_level=fadeLevel, \ + delay=item.effectBlock.dwStartDelay / timediv)) + id[nefx] = sdl2.SDL_HapticNewEffect(haptic, efx[nefx]) + if id[nefx] < 0: + print " Error", sdl2.SDL_GetError() + nefx += 1 + + time = (item.effectBlock.dwDuration + item.effectBlock.dwStartDelay) / timediv + if time > stoptime: + stoptime = time + + # play all simulataneously + for i in range(0, nefx): + print "Playing effect", i + ret = sdl2.SDL_HapticRunEffect(haptic, id[i], 1) + if ret < 0: + print " Error", sdl2.SDL_GetError() + + # wait for all effects to complete + sdl2.SDL_Delay(stoptime) + + # clean up + for i in range(0, nefx): + sdl2.SDL_HapticStopEffect(haptic, id[i]); + sdl2.SDL_HapticDestroyEffect(haptic, id[i]) + +#--- + +if __name__ == "__main__": + parser = argparse.ArgumentParser(prog="ffe_parse") + parser.add_argument("filename", nargs=1) + parser.add_argument("-d", "--dump", action='store_true', help="decode file and output to screen") + parser.add_argument("-w", "--write", dest="write", help="write data to '.ffe' file") + + # SDW - edit test + parser.add_argument("-a", "--axis", action='store_true', help="toggle number of axis") + parser.add_argument("-r", "--rotate", dest="rotate", help="rotate polar by Degrees") + + if (_hasPySDL2): + parser.add_argument("-p", "--play", action='store_true', help="play effect with SDL2") + parser.add_argument("-D", "--device", dest="device", default=0, help="SDL2 device to use (integer 0-)") + parser.add_argument("-l", "--linux", action='store_true', help="correct cartesian co-ords for linux") + + options = parser.parse_args() + + datafile = open(options.filename[0]) + ffe = header.parse(datafile.read(65536)) # Can't imagine these files would be any larger + datafile.close() + + if options.rotate: + for effect in ffe.listBlock: + if effect.effectBlock.dwFlags.DIEFF_POLAR: + effect.dataDirection.direction[0] += int(options.rotate) * 100 + + if options.axis: + for effect in ffe.listBlock: + if effect.effectBlock.cAxes == 2: + effect.effectBlock.cAxes = 1 + elif effect.effectBlock.cAxes == 1: + effect.effectBlock.cAxes = 2 + + # shorten/lengthen arrays + effect.dataAxis.axis.extend(effect.dataAxis.axis[-1:]*(effect.effectBlock.cAxes-len(effect.dataAxis.axis))) + effect.dataAxis.axis = effect.dataAxis.axis[:effect.effectBlock.cAxes] + + effect.dataDirection.direction.extend(effect.dataDirection.direction[-1:]*(effect.effectBlock.cAxes-len(effect.dataDirection.direction))) + effect.dataDirection.direction = effect.dataDirection.direction[:effect.effectBlock.cAxes] + + if (effect.Guid == guid_spring or \ + effect.Guid == guid_damper or \ + effect.Guid == guid_inertia or \ + effect.Guid == guid_friction): + effect.dataSpecific.dataSpecificSpringArray.extend(effect.dataSpecific.dataSpecificSpringArray[-1:]* \ + (effect.effectBlock.cAxes-len(effect.dataSpecific.dataSpecificSpringArray))) + effect.dataSpecific.dataSpecificSpringArray = effect.dataSpecific.dataSpecificSpringArray[:effect.effectBlock.cAxes] + + # recompute block lengths + fix_block_lengths(effect) + + if options.dump: + print "Parsing file:", options.filename[0] + print "---" + print ffe + + if options.write: + datafile = open(options.write, 'w') + datafile.write(header.build(ffe)) + datafile.close() + + if _hasPySDL2: + if options.play: + play_effect(ffe, int(options.device), options.linux) \ No newline at end of file -- 2.43.5