Final Fantasy Hacktics

Modding => PSX FFT Hacking => Topic started by: lirmont on July 04, 2014, 02:35:12 am

Title: Waveform/Instrument Injection
Post by: lirmont on July 04, 2014, 02:35:12 am
New instruments in FFT!



--

Have not decoded the actual definitions for instruments yet. Have managed to replace the wave data (WAVESET.WD) that they operate on.

Involved files:


Process:


Usage (for the test in the video on a Japanese ISO of FFT).
Code (Command Line) Select
python wave-to-playstation-encoding.py C1.wav 6816 C1.txt 0x360
python wave-to-playstation-encoding.py C2.wav 3808 C2.txt 0xb0
python wave-to-playstation-encoding.py C3.wav 3936 C3.txt 0x1c0
python wave-to-playstation-encoding.py C4.wav 1472 C4.txt 0x250
python wave-to-playstation-encoding.py C5.wav 3104 C5.txt 0x480


wave-to-playstation-encoding.py
NOTE: Requires installing Python and several dependencies: numpy and scipy.

Code (Python) Select

import numpy as np
from itertools import product
from scipy.io.wavfile import read

# Pass through values that line up with offsets: 0x0 to 0xf.
acceptedValues = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, -1.0, -0.875, -0.75, -0.625, -0.5, -0.375, -0.25, -0.125]
acceptedValuesMaxArray = np.array([0.875, -1, ])
allValueRange = np.array(acceptedValues)
# SYNONYMS: 0x06 and 0x03. 0x06 may be some kind of off switch to something 0x02 can do in effects. 0x03 signals the last block follows. They don't do anything special to the data, though.
amplitudeCommands = [0x02, ]
# Cutoff seemed like < 0x5d. Also, cannot use anything evenly divisible by any in range: (0xd, 0xe, 0xf). However, everything above the first 16 (with the exception of the 0x5x block, which is a pass through exactly? the same as 00) are effects, and can safely be ignored for the purposes of writing pre-amplified data into the waveform.
exponents = [i for i in range(0, 0x10) if (i == 0) or ((i % 0xd) > 0) and ((i % 0xe) > 0) and ((i% 0xf) > 0)]
negativePowersOfTwo = list(product(amplitudeCommands, exponents))

# Yield successive n-sized chunks from l. Used to slice bytes of an incoming wave into 28-sample sets (encodes to 14 bytes).
def chunks(l, n):
for i in xrange(0, len(l), n):
yield l[i:i+n]

# Convert sample value (expects 16-bit signed integer) into a value from -1 to 1.
def getSampleValue(n):
thisSampleValue = np.nan_to_num(n) / 32767.0
return thisSampleValue

# Rounds actual data to data represented in the encoding scheme.
def roundToNearestAcceptedValue(sampleValue, valueRange = None):
valueRange = valueRange if valueRange is not None else allValueRange
delta = None
actualNumber = None
for possibility in valueRange:
thisDelta = abs(sampleValue - possibility)
#
if (delta is None) or (thisDelta < delta):
delta = thisDelta
actualNumber = possibility
return actualNumber

# For values of 0x0 to 0xc in command 0x02, yield: 2**(-n). Values in 0x1x range are a concave falloff. Values in 0x2x range are a convex falloff. Values in 0x3x range are a whole sine wave. Values in 0x4x range are the first 4/5th's of a sine wave.
def getModifier(value):
return 1 / float(2 ** value)

# Get the closest amplitude modifier in the encoding scheme. Intended for use with maximum values.
def getClosestModifier(value, valueRange = acceptedValuesMaxArray, negativePowersOfTwo = negativePowersOfTwo):
# If the value is not zero, go looking for it as a max/min value in the sliding table of values.
if value != 0:
delta = None
actualNumber = None
actualModifier = None
for idx, (command, power) in enumerate(negativePowersOfTwo):
try:
# Typically, get the value of: 1 / 2**power.
modifier = getModifier(power)
# See if any of the maximum values are now equivalent to the supplied value.
for possibility in (valueRange * modifier):
thisDelta = abs(value - possibility)
# If this value is closer than previous values, store it as new solution to best-fit question.
if (delta is None) or (thisDelta < delta):
# Try to escape early. The delta is an exact match.
if thisDelta == 0:
return actualModifier
# Otherwise, store best and continue.
else:
delta = thisDelta
actualNumber = possibility
actualModifier = idx
except:
pass
return actualModifier
# If the value is zero, it can be represented in the pass through, which will be data set (2, 0) at index 0 of negativePowersOfTwo.
else:
return 0

# Translate the 28-sample set into a 16-byte data stream of form: AACC 00112233445566778899AABBCCDD
def getBytesForStream(dataStream):
samples = np.array([getSampleValue(rawValue) for rawValue in dataStream])
maximumValue = samples[abs(samples).argmax()] #max([abs(sample) for sample in samples])
# Store bytes for wave amplitude modifier and stream.
modifierBytes = [0x0, 0x2]
bytes = []
# If the maximum value is not in the default max values list, look it up. Majority case.
modifierIndex = getClosestModifier(maximumValue)
command, power = negativePowersOfTwo[modifierIndex]
thisModifier = getModifier(power)
modifierBytes = [power, command]
scaledDefaultValues = allValueRange * thisModifier
roundedValues = [roundToNearestAcceptedValue(sampleValue, valueRange = scaledDefaultValues) for sampleValue in samples]
# Find indices of values in list.
scaledDefaultValues = list(scaledDefaultValues)
indices = [scaledDefaultValues.index(roundedValue) for roundedValue in roundedValues]
# Get (1, 2), (3, 4), ... (n, n + 1).
groupedIndices = [(indices[i], indices[i+1]) for i in range(0, len(indices), 2)]
# Write the bytes out.
bytes = [] + modifierBytes
for start, finish in groupedIndices:
# Calculate the 2-dimensional array offset in hex of the sliding table values.
x = start * (16 ** 0) + finish * (16 ** 1)
bytes.append(x)
#
return bytes

# Process all the data of the incoming wave.
def processData(data, samplesPerByte = 2, waveFormBytesPerStream = 14, limit = None):
# Typically: 2 compression modulation bytes followed by compressing 28 bytes into 14 bytes such that 1 byte equals 2 samples. Total: 16 bytes per stream.
fourteenByteDataStreams = chunks(data, waveFormBytesPerStream * samplesPerByte)
# Store return data in string (intended for pasting in a hex editor).
result = ""
totalBytes = 0
# For each stream, bet the bytes.
for dataStream in fourteenByteDataStreams:
theseBytes = getBytesForStream(dataStream)
count = len(theseBytes)
# If this is the last stream to be written, signal with 0x03 instead of 0x02 (or 0x06). Then quit.
if (limit is not None) and (totalBytes + count) >= limit:
theseBytes = theseBytes[0:limit - totalBytes]
theseBytes[1] = 0x03
result += "".join(["%02X" % byte for byte in theseBytes])
totalBytes += len(theseBytes)
break
# Otherwise, add in form of AACC00112233445566778899AABBCCDD.
else:
result += "".join(["%02X" % byte for byte in theseBytes])
totalBytes += len(theseBytes)
return result

# For arguments, accept: 37 (or) 0x25, 0x800 (or) 2048, etc.
def hexOrInteger(inputString):
try:
return int(inputString)
except ValueError:
return int(inputString, 16)
return 0

# Usage: python wave-to-playstation-encoding.py WaveFile.wav
# Crossed Streams Usage (short): python wave-to-playstation-encoding.py WaveFile.wav OtherStream.txt InitialWriteFor
# Crossed Streams Usage: python wave-to-playstation-encoding.py WaveFile.wav OtherStream.txt InitialWriteFor OtherStreamWriteFor ThisStreamWriteFor
if __name__ == "__main__":
import sys
# Arguments: No input file.
if len(sys.argv) == 1:
print "No input file. Usage: python %s WaveFile.wav\nCross Stream Usage: python %s WaveFile.wav OtherStream.txt InitialWriteFor OtherStreamWriteFor ThisStreamWriteFor\nNOTE: ANSI files only." % (sys.argv[0], sys.argv[0])
sys.exit()
# Arguments: No length.
if len(sys.argv) == 2:
sys.argv.append(None)
# Turn length into an integer.
else:
sys.argv[2] = hexOrInteger(sys.argv[2])
# Arguments: Cross stream file.
if len(sys.argv) == 3:
sys.argv.append(None)
# Arguments: Cross stream initial write for.
if len(sys.argv) == 4:
sys.argv.append(0)
# Turn initial write for into integer.
else:
sys.argv[4] = hexOrInteger(sys.argv[4])
# Arguments: Cross into other stream for.
if len(sys.argv) == 5:
sys.argv.append(0x130)
# Turn cross into other stream for into integer.
else:
sys.argv[5] = hexOrInteger(sys.argv[5])
# Arguments: Cross back into this stream for.
if len(sys.argv) == 6:
sys.argv.append(0x800)
# Turn cross back into this stream for into integer.
else:
sys.argv[6] = hexOrInteger(sys.argv[6])
# Arguments.
thisFilename, waveFilename, length, fileWithStreamFromISO, initialWriteFor, otherStreamWriteFor, thisStreamWriteFor = sys.argv
# Print back the filename for reference.
print waveFilename
# Read data.
rate, data = read(waveFilename)
# Get the bytes of the file body.
result = processData(data, limit = length)
if fileWithStreamFromISO is not None:
crossedStream = ""
parts = []
currentFileCaret = 0
currentResultCaret = 0
resultLength = len(result)
# Read the other stream.
with open(fileWithStreamFromISO, "r") as f:
stream = f.read()
# Initial write (write X bytes of the result).
count = initialWriteFor * 2
currentFileCaret += count
currentResultCaret += count
parts.append(result[0:currentFileCaret])
# Enter into a loop of crossing back and forth between the other stream and the result stream.
while currentResultCaret < resultLength:
# Write from other stream.
count = otherStreamWriteFor * 2
parts.append(stream[currentFileCaret:currentFileCaret+count])
currentFileCaret += count
# Write from this stream.
if currentResultCaret < resultLength:
count = thisStreamWriteFor * 2
actualEnd = min(resultLength, currentResultCaret + count)
count = (actualEnd - currentResultCaret)
parts.append(result[currentResultCaret:actualEnd])
currentFileCaret += count
currentResultCaret += count
crossedStream = "".join(parts)
# Print the byte count of the combined streams.
print "Bytes:", (len(crossedStream) / 2)
# Write the bytes out to a text file (for copying into a hex editor).
with open("%s.bytes" % waveFilename, "w+") as f:
f.write(crossedStream)
else:
# Print the byte count of the result.
print "Bytes:", (len(result) / 2)
# Write the bytes out to a text file (for copying into a hex editor).
with open("%s.bytes" % waveFilename, "w+") as f:
f.write(result)
Title: Re: Waveform/Instrument Injection
Post by: Choto on July 04, 2014, 11:05:42 am
Ummm.... Holy Crap? This kinda came out of nowhere, definitely gotta give it a spin!

Hopefully this will shed some light on effect file sound waveforms too.. I've been trying to figure them out for awhile... Thanks man!
Title: Re: Waveform/Instrument Injection
Post by: lirmont on July 05, 2014, 09:53:03 pm
Something else maybe worth looking at is that PSound finds 2 really short audio files in BATTLE.BIN. I found them at 0x2d4e0-0x2d760 (185568-186208) and 0x2eaa7-0x2eca6 (191143-191654). Might be an easy target for a quick customization. I think the first one might be the gil award plink (it's 0.05s). I don't know what the second one is, but it's 0.04s.