# Copyright 2019 RoadrunnerWMC
#
# This file is part of ndspy.
#
# ndspy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ndspy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ndspy. If not, see <https://www.gnu.org/licenses/>.
"""
Support for SWAR sound wave archives.
"""
import struct
from . import WaveType
from . import _common
from . import soundWave
[docs]class SWAR:
"""
A SWAR sound wave archive.
"""
# When saving SDAT, two otherwise identical SWARs will share data
# only if their dataMergeOptimizationIDs are the same.
# You can pretty safely ignore this.
dataMergeOptimizationID = 0
def __init__(self, file=None, unk02=0):
self.unk02 = unk02
self.waves = []
if file is not None:
if not file.startswith(b'SWAR'):
raise ValueError("Wrong magic (should be b'SWAR', instead "
f'found {file[:4]})')
self._initFromData(file)
def _initFromData(self, data):
"""
Initialize the SWAR from file data.
"""
magic, bom, version, filesize, headersize, numblocks = \
_common.NDS_STD_FILE_HEADER.unpack_from(data, 0)
if version != 0x100:
raise ValueError(f'Unsupported SWAR version: {version}')
assert magic == b'SWAR', f'Incorrect SWAR magic ({magic})'
dataMagic, dataSize, swavCount = \
struct.unpack_from('<4sI32xI', data, 0x10)
assert dataMagic == b'DATA', f'Incorrect SWAR DATA magic ({dataMagic})'
dataArrayPos = 0x3C
for i in range(swavCount):
swavOffset, nextOffset = \
struct.unpack_from('<II', data, dataArrayPos)
dataArrayPos += 4
if i == swavCount - 1:
# DON'T use len(data) here -- there can sometimes be
# extra pad at the end, which will mess this up!
nextOffset = filesize
swavData = data[swavOffset:nextOffset]
waveType, loopFlag, sampleRate, time, loopOffset, totalLength = \
struct.unpack_from('<B?3HI', swavData)
swav = soundWave.SWAV.fromData(swavData[0xC:],
waveType=WaveType(waveType),
isLooped=loopFlag,
sampleRate=sampleRate,
time=time,
loopOffset=loopOffset,
totalLength=loopOffset + totalLength)
self.waves.append(swav)
[docs] @classmethod
def fromWaves(cls, waves, unk02=0):
"""
Create a SWAR from a list of SWAVs.
"""
obj = cls(unk02=unk02)
obj.waves = waves
return obj
[docs] @classmethod
def fromFile(cls, filePath, unk02=0):
"""
Load a SWAR from a filesystem file.
"""
with open(filePath, 'rb') as f:
return cls(f.read(), unk02=0)
[docs] def save(self):
"""
Generate file data representing this SWAR, and then return that
data and .unk02 as a pair (2-tuple). This matches the parameters
of the default class constructor.
"""
fileLen = 0x3C + sum(4 + 0xC + len(s.data) for s in self.waves)
data = bytearray()
data.extend(_common.NDS_STD_FILE_HEADER.pack(
b'SWAR', 0xFEFF, 0x100, fileLen, 0x10, 1))
data.extend(struct.pack(
'<4sI32xI', b'DATA', fileLen - 0x10, len(self.waves)))
lenOffsetsTable = 4 * len(self.waves)
offsets = []
swavsTable = bytearray()
for swav in self.waves:
offsets.append(0x3C + lenOffsetsTable + len(swavsTable))
swavData = swav.save()[0x18:] # Chop off the file header
# but not the SWAVInfo struct
swavsTable.extend(swavData)
offsetsTable = struct.pack(f'<{len(offsets)}I', *offsets)
data.extend(offsetsTable)
data.extend(swavsTable)
return (data, self.unk02)
[docs] def saveToFile(self, filePath):
"""
Generate file data representing this SWAR, and save it to a
filesystem file.
"""
d = self.save()[0]
with open(filePath, 'wb') as f:
f.write(d)
def __str__(self):
linesList = [f'<swar']
linesList.extend(_common.enumeratedListOfStrs(self.waves))
linesList.append('>')
return '\n'.join(linesList)
def __repr__(self):
return f'{type(self).__name__}.fromWaves({self.waves!r})'