• Welcome to Final Fantasy Hacktics. Please login or sign up.
 
March 19, 2024, 07:52:39 am

News:

Please use .png instead of .bmp when uploading unfinished sprites to the forum!


Show posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.

Topics - lirmont

1
PSX FFT Hacking / Waveform/Instrument Injection
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:

  • WAVESET.WD (wave data)

  • The ISO it's on.



Process:

  • Drag WAVESET.WD into the PSound program.

  • PSound will show 85 waves (0..84).

  • Pick the sound you want to replace.

  • Export the sound (after configuring PSound to not add extra lead out).

  • Document how long the sound is for reference in picking a replacement .WAV file (NOTE: .WAV file should be 22050Hz, 16-bit PCM).

  • Open WAVESET.WD in a hex editor.

  • The first waveform in that file is at 0x0b40; you can identify waves after this point in the file by a sequence of 16 or more consecutive empty bytes; the data streams start as soon as the first non-zero 2-byte set is encountered (like 0x00 0x02 or 0x05 0x02).

  • Jump to the waveform you want to replace.

  • Determine the length to the next set of 16 empty bytes from the start of the data set. This number is the length in bytes of the data streams in the waveform.

  • Use the following program I wrote to encode your wave file like: python wave-to-playstation-encoding.py WaveFileName.wav LengthInBytes

  • There is now a file containing the data: WaveFileName.wav.bytes

  • In a hex editor that supports paste overwrite (re: the HxD program), paste over the existing data streams back in the WAVESET.WD file.

  • Clear PSound's playlist, and reload the file by dragging it into the playlist area.

  • If you copied the data over correctly, you'll hear the new wave. If not, you'll hear a stock, choppy wave that PSound plays when it can't decode the data (you can just think of it as an error beep).

  • When you're happy with it, you'll need to replace the WAVESET.WD file in the ISO.

  • To do this part manually, open your ISO in a hex editor, and search for the text string: dwdsP (or the hex string 64776473502016CE). This is the start of the WAVESET.WD file in the ISO.

  • Locate the wave you want to replace after that point.

  • Waveforms contain an unbroken line of 2-byte commands that you can see clearly in a standard 16-byte view (re: look for the column of 0x02, 0x06, or 0x03 bytes). Highlight the data stream until you encounter non-file data (i.e. where there would be a two byte command like 0x00 0x02 there is instead a FE FF). Record this value as the initial write for the file.

  • Now, continue highlighting all the way down to just before the next set of consecutive 16 empty bytes (0x03 in the second command byte signals the last stream of the waveform). Copy this data into an ANSI text file (like 0010.txt), removing the spaces between bytes with a replace all tool.

  • Now, re-encode the replacement WAV file against the ISO's data, like: python wave-to-playstation-encoding.py WaveFileName.wav LengthInBytes TextFileWithBytesFromISO.txt InitialWriteAsNumberOrHexadcimal

  • The content of the resulting .bytes file can be copied over the data stream in the ISO in the same way as the file (paste write).

  • Your ISO now contains a different sound than FFT shipped with.



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)
2
PSX FFT Hacking / SMD Parser (WIP)
June 25, 2014, 02:41:21 pm
I know there's already something that will get a MIDI into SMD format. I was curious to get the notes out of an SMD, though. This is the file "MUSIC_24.SMD" (LvUp L) with varying MIDI instruments: MUSIC_24.mp3.

As soon as I finish cleaning up the code, I'll post a link here. I believe some of the unknowns point to the information the BGM test shows (an actual title, author, and help text), but I wasn't that curious.

Name: music_24
Tracks: 10
Path: MUSIC_24.SMD
Start Track #1
    Set tempo to 128 (128/102 * 120bpm).
    Set time signature to 4/4.
    BEGIN LOOP, Loop x3
        Rest for 192/192 of a whole note.
    END LOOP
End Track.
Start Track #2
    Set volume to 30/127.
    Set balance to 64/127 towards right speaker.
    Set instrument to 79.
    Set octave to 5.
    C5 (74%) for 6% of a whole note, NOTE ON
    Rest for 60/192 of a whole note.
    F5 (74%) for 6% of a whole note, NOTE ON
    Rest for 84/192 of a whole note.
    C5 (74%) for 6% of a whole note, NOTE ON
    Rest for 12/192 of a whole note.
    Rest for 48/192 of a whole note.
    F5 (74%) for 6% of a whole note, NOTE ON
    Rest for 36/192 of a whole note.
    Set instrument to 80.
    C#5 (74%) for 12% of a whole note, NOTE ON
    Rest for 24/192 of a whole note.
    Set instrument to 79.
    C#5 (74%) for 6% of a whole note, NOTE ON
    Rest for 36/192 of a whole note.
    C5 (74%) for 6% of a whole note, NOTE ON
    Rest for 144/192 of a whole note.
    Rest for 36/192 of a whole note.
End Track.
Start Track #3
    Set volume to 30/127.
    Set balance to 64/127 towards right speaker.
    Rest for 48/192 of a whole note.
    Set instrument to 80.
    Set octave to 4.
    G4 (74%) for 6% of a whole note, NOTE ON
    Rest for 60/192 of a whole note.
    A#4 (74%) for 6% of a whole note, NOTE ON
    Rest for 48/192 of a whole note.
    Rest for 12/192 of a whole note.
    Rest for 24/192 of a whole note.
    G4 (74%) for 12% of a whole note, NOTE ON
    Rest for 24/192 of a whole note.
    Increment octave from 4 to 5.
    C5 (74%) for 6% of a whole note, NOTE ON
    Rest for 36/192 of a whole note.
    Decrement octave from 5 to 4.
    A#4 (74%) for 6% of a whole note, NOTE ON
    Rest for 36/192 of a whole note.
    Set instrument to 79.
    A#4 (74%) for 6% of a whole note, NOTE ON
    Rest for 12/192 of a whole note.
    Rest for 192/192 of a whole note.
End Track.
Start Track #4
    Set volume to 36/127.
    Set balance to 64/127 towards right speaker.
    Set instrument to 70.
    Set octave to 6.
    C6 (78%) for 4% of a whole note, NOTE ON
    Rest for 64/192 of a whole note.
    Decrement octave from 6 to 5.
    G#5 (78%) for 3% of a whole note, NOTE ON
    Rest for 90/192 of a whole note.
    Increment octave from 5 to 6.
    C6 (78%) for 4% of a whole note, NOTE ON
    Rest for 16/192 of a whole note.
    Rest for 48/192 of a whole note.
    Decrement octave from 6 to 5.
    G#5 (78%) for 3% of a whole note, NOTE ON
    Rest for 42/192 of a whole note.
    C#5 (78%) for 12% of a whole note, NOTE ON
    Rest for 24/192 of a whole note.
    A#5 (78%) for 3% of a whole note, NOTE ON
    Rest for 36/192 of a whole note.
    Rest for 6/192 of a whole note.
    Increment octave from 5 to 6.
    C6 (78%) for 4% of a whole note, NOTE ON
    Rest for 144/192 of a whole note.
    Rest for 36/192 of a whole note.
    Rest for 4/192 of a whole note.
End Track.
Start Track #5
    Set volume to 36/127.
    Set balance to 64/127 towards right speaker.
    Set instrument to 70.
    Set octave to 5.
    G5 (78%) for 4% of a whole note, NOTE ON
    Rest for 64/192 of a whole note.
    D#5 (78%) for 3% of a whole note, NOTE ON
    Rest for 90/192 of a whole note.
    G5 (78%) for 4% of a whole note, NOTE ON
    Rest for 16/192 of a whole note.
    Rest for 48/192 of a whole note.
    D#5 (78%) for 3% of a whole note, NOTE ON
    Rest for 42/192 of a whole note.
    Decrement octave from 5 to 4.
    A#4 (78%) for 12% of a whole note, NOTE ON
    Rest for 24/192 of a whole note.
    Increment octave from 4 to 5.
    F#5 (78%) for 3% of a whole note, NOTE ON
    Rest for 36/192 of a whole note.
    Rest for 6/192 of a whole note.
    G5 (78%) for 4% of a whole note, NOTE ON
    Rest for 144/192 of a whole note.
    Rest for 36/192 of a whole note.
    Rest for 4/192 of a whole note.
End Track.
Start Track #6
    Set volume to 36/127.
    Set balance to 64/127 towards right speaker.
    Set instrument to 70.
    Set octave to 5.
    E5 (78%) for 4% of a whole note, NOTE ON
    Rest for 64/192 of a whole note.
    C5 (78%) for 3% of a whole note, NOTE ON
    Rest for 90/192 of a whole note.
    E5 (78%) for 4% of a whole note, NOTE ON
    Rest for 16/192 of a whole note.
    Rest for 48/192 of a whole note.
    C5 (78%) for 3% of a whole note, NOTE ON
    Rest for 42/192 of a whole note.
    Set instrument to 68.
    F#5 (78%) for 12% of a whole note, NOTE ON
    Rest for 24/192 of a whole note.
    Set instrument to 70.
    C#5 (78%) for 3% of a whole note, NOTE ON
    Rest for 36/192 of a whole note.
    Rest for 6/192 of a whole note.
    E5 (78%) for 4% of a whole note, NOTE ON
    Rest for 144/192 of a whole note.
    Rest for 36/192 of a whole note.
    Rest for 4/192 of a whole note.
End Track.
Start Track #7
    Set volume to 36/127.
    Set balance to 64/127 towards right speaker.
    Rest for 48/192 of a whole note.
    Set instrument to 70.
    Set octave to 5.
    G5 (78%) for 3% of a whole note, NOTE ON
    Rest for 66/192 of a whole note.
    F5 (78%) for 4% of a whole note, NOTE ON
    Rest for 64/192 of a whole note.
    Rest for 24/192 of a whole note.
    G5 (78%) for 12% of a whole note, NOTE ON
    Rest for 24/192 of a whole note.
    F5 (78%) for 3% of a whole note, NOTE ON
    Rest for 42/192 of a whole note.
    F#5 (78%) for 3% of a whole note, NOTE ON
    Rest for 42/192 of a whole note.
    Set instrument to 71.
    Increment octave from 5 to 6.
    C#6 (78%) for 3% of a whole note, NOTE ON
    Rest for 18/192 of a whole note.
    Rest for 192/192 of a whole note.
End Track.
Start Track #8
    Set volume to 36/127.
    Set balance to 64/127 towards right speaker.
    Rest for 48/192 of a whole note.
    Set instrument to 70.
    Set octave to 5.
    E5 (78%) for 3% of a whole note, NOTE ON
    Rest for 66/192 of a whole note.
    D5 (78%) for 4% of a whole note, NOTE ON
    Rest for 64/192 of a whole note.
    Rest for 24/192 of a whole note.
    E5 (78%) for 12% of a whole note, NOTE ON
    Rest for 24/192 of a whole note.
    D5 (78%) for 3% of a whole note, NOTE ON
    Rest for 42/192 of a whole note.
    C#5 (78%) for 3% of a whole note, NOTE ON
    Rest for 42/192 of a whole note.
    A#5 (78%) for 3% of a whole note, NOTE ON
    Rest for 18/192 of a whole note.
    Rest for 192/192 of a whole note.
End Track.
Start Track #9
    Set volume to 36/127.
    Set balance to 64/127 towards right speaker.
    Rest for 48/192 of a whole note.
    Set instrument to 70.
    Set octave to 5.
    C5 (78%) for 3% of a whole note, NOTE ON
    Rest for 66/192 of a whole note.
    Decrement octave from 5 to 4.
    A#4 (78%) for 4% of a whole note, NOTE ON
    Rest for 64/192 of a whole note.
    Rest for 24/192 of a whole note.
    Increment octave from 4 to 5.
    C5 (78%) for 12% of a whole note, NOTE ON
    Rest for 24/192 of a whole note.
    BEGIN LOOP, Loop x2
        Decrement octave from 5 to 4.
        A#4 (78%) for 3% of a whole note, NOTE ON
        Rest for 42/192 of a whole note.
    END LOOP
    Increment octave from 4 to 5.
    F#5 (78%) for 3% of a whole note, NOTE ON
    Rest for 18/192 of a whole note.
    Rest for 192/192 of a whole note.
End Track.
Start Track #10
    BEGIN LOOP, Loop x3
        Rest for 192/192 of a whole note.
    END LOOP
End Track.