--- /dev/null
+# 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