PageRenderTime 106ms CodeModel.GetById 35ms RepoModel.GetById 0ms app.codeStats 0ms

/Python/scripts/sounddecompress.py

https://github.com/Deledrius/libhsplasma
Python | 298 lines | 199 code | 48 blank | 51 comment | 61 complexity | a8f4822f8a5333ec7e31d21f0c6a94c9 MD5 | raw file
  1. #!/usr/bin/env python
  2. # This file is part of HSPlasma.
  3. #
  4. # HSPlasma is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # HSPlasma is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with HSPlasma. If not, see <http://www.gnu.org/licenses/>.
  16. """sounddecompress.py
  17. A Utility for decompressing Uru audio
  18. by Joseph Davies (deledrius@gmail.com)
  19. * Requires sox (http://sox.sourceforge.net/)
  20. * Requires libHSPlasma and PyHSPlasma (https://github.com/H-uru/libhsplasma
  21. with Python support.)
  22. Usage:
  23. ./sounddecompress.py -u /path/to/uru
  24. (Override base Uru using the default subdirectories)
  25. ./sounddecompress.py -d /path/to/agefiles -s /path/to/oggs -c /path/to/outputwavs
  26. (Override default subdirectories)
  27. ./sounddecompress.py -u /path/to/uru -c /path/to/outputwavs
  28. (Override base Uru and output directory only)
  29. """
  30. from __future__ import print_function
  31. import os
  32. import sys
  33. import glob
  34. import math
  35. import subprocess
  36. from xml.dom.minidom import parse, getDOMImplementation
  37. from optparse import OptionParser, OptionGroup
  38. try:
  39. import PyHSPlasma
  40. except ImportError as e:
  41. libPlasma = False
  42. else:
  43. libPlasma = True
  44. version = 1.11
  45. ## Default Paths
  46. DefaultUruDir = "."
  47. DefaultDataDir = "dat"
  48. DefaultSFXDir = "sfx"
  49. DefaultCacheDir = os.path.join(DefaultSFXDir,"streamingCache")
  50. ## Initialize empty queue of sound files to decompress
  51. queue = {}
  52. def getDecompressQueue(datadir):
  53. if not libPlasma:
  54. print("\nFatal Error: PyHSPlasma module not loaded. Reading of Age files unavailable.")
  55. return False
  56. ## Only display Errors
  57. PyHSPlasma.plDebug.Init(PyHSPlasma.plDebug.kDLError)
  58. ## Create our Resource Manager
  59. plResMgr = PyHSPlasma.plResManager()
  60. ## Get Age list for progress
  61. print("Loading Age files...")
  62. numAges = len(glob.glob(os.path.join(datadir, "*.age")))
  63. numAgesRead = 0
  64. if numAges == 0:
  65. print("No Age files found. Quitting.")
  66. return False
  67. print("{0} Ages found in {1}.\nScanning...".format(numAges, datadir))
  68. ## Flip through all ages
  69. progress(50, float(numAgesRead)/float(numAges) * 100)
  70. for ageFile in glob.iglob(os.path.join(datadir,"*.age")):
  71. fullpath = os.path.abspath(ageFile)
  72. try:
  73. age = plResMgr.ReadAge(fullpath, True)
  74. except IOError as e:
  75. print("Warning - Unable to read Age: {0}".format(ageFile), file=sys.stderr)
  76. except KeyboardInterrupt:
  77. print("\nInterrupt detected. Aborting.")
  78. return False
  79. else:
  80. try:
  81. ## Get the plSoundBuffer in each page and queue sounds
  82. ## which are not set to StreamCompressed
  83. for pageNum in range(0, age.getNumPages()):
  84. page = plResMgr.FindPage(age.getPageLoc(pageNum, plResMgr.getVer()))
  85. if (page == None):
  86. raise Exception("Unable to completely load age "+age.name+": Can't find page "+str(age.getPageLoc(pageNum, plResMgr.getVer())))
  87. if PyHSPlasma.plFactory.kSoundBuffer in plResMgr.getTypes(page.location):
  88. for key in plResMgr.getKeys(page.location, PyHSPlasma.plFactory.kSoundBuffer):
  89. soundBuffer = key.object
  90. if soundBuffer.fileName in queue.keys():
  91. channelOptions = queue[soundBuffer.fileName]
  92. else:
  93. channelOptions = {}
  94. if (soundBuffer.flags & soundBuffer.kOnlyRightChannel): channel = channelOptions["Right"] = True
  95. if (soundBuffer.flags & soundBuffer.kOnlyLeftChannel): channel = channelOptions["Left"] = True
  96. if channelOptions == {}: channelOptions["Both"] = True
  97. if not (soundBuffer.flags & soundBuffer.kStreamCompressed):
  98. queue[soundBuffer.fileName] = channelOptions
  99. except MemoryError as e:
  100. print("\nFatal Error - Unable to process Age ({0}) - {1}".format(age.name, e), file=sys.stderr)
  101. return False
  102. except KeyboardInterrupt:
  103. print("\nInterrupt detected. Aborting.")
  104. return False
  105. plResMgr.UnloadAge(age.name)
  106. ## Onto the next
  107. numAgesRead = numAgesRead + 1
  108. progress(50, float(numAgesRead)/float(numAges) * 100)
  109. print("{0} sound files added to queue.".format(len(queue)))
  110. return True
  111. def doDecompress(sfxdir, cachedir):
  112. ## Make sure the cache dir exists
  113. if not os.path.exists(os.path.abspath(cachedir)):
  114. print("Creating streamingCache directory.")
  115. os.mkdir(cachedir)
  116. ## Prepare progress
  117. print("Decompressing audio files...")
  118. numFilesProcessed = 0
  119. numFiles = len(queue)
  120. ## Flip through all sound files
  121. progress(50, float(numFilesProcessed)/float(numFiles) * 100)
  122. for oggFile in queue.keys():
  123. inpath = os.path.abspath(os.path.join(sfxdir, oggFile))
  124. if os.path.exists(inpath):
  125. for channel in queue[oggFile]:
  126. if channel == "Both":
  127. wavFile = oggFile.split('.')[0] + ".wav"
  128. soxCommand = "sox \"{0}\" \"{1}\""
  129. elif channel == "Left":
  130. wavFile = oggFile.split('.')[0] + "-Left.wav"
  131. soxCommand = "sox \"{0}\" -c 1 \"{1}\" mixer -l"
  132. elif channel == "Right":
  133. wavFile = oggFile.split('.')[0] + "-Right.wav"
  134. soxCommand = "sox \"{0}\" -c 1 \"{1}\" mixer -r"
  135. outpath = os.path.abspath(os.path.join(cachedir, wavFile))
  136. soxCommand = soxCommand.format(inpath, outpath)
  137. ## Conversion from OGG to WAV
  138. try:
  139. retcode = subprocess.check_call(soxCommand, shell=True, stderr=open(os.devnull,"w"))
  140. except subprocess.CalledProcessError as e:
  141. print("\nFatal Error - Unable to execute Sox!\n", file=sys.stderr)
  142. return False
  143. except OSError as e:
  144. print("\nFatal Error - Unable to execute Sox!\n", file=sys.stderr)
  145. return False
  146. except KeyboardInterrupt:
  147. print("\nInterrupt detected. Aborting.")
  148. return False
  149. ## Onto the next
  150. numFilesProcessed = numFilesProcessed + 1
  151. progress(50, float(numFilesProcessed)/float(numFiles) * 100)
  152. return True
  153. ## XML Queue Writer
  154. def writeQueueToXML(outfile):
  155. print("Writing queue to xml: {0}".format(outfile))
  156. root = getDOMImplementation().createDocument(None, "conversionlist", None)
  157. with open(os.path.abspath(outfile), "w") as xmlfile:
  158. print("Writing XML file \"{0}\"".format(outfile))
  159. root.documentElement.setAttribute("version", str(version))
  160. root.documentElement.setAttribute("game", "Unknown")
  161. root.documentElement.setAttribute("gamebuild", "Unknown")
  162. numFilesProcessed = 0
  163. numFiles = len(queue)
  164. progress(50, float(numFilesProcessed)/float(numFiles) * 100)
  165. for soundfile in queue.keys():
  166. sfNode = root.createElement("soundfile")
  167. sfNode.setAttribute("name", soundfile)
  168. coNode = root.createElement("channeloutputs")
  169. coNode.setAttribute("stereo", str(queue[soundfile].has_key("Both") and queue[soundfile]["Both"]))
  170. coNode.setAttribute("left", str(queue[soundfile].has_key("Left") and queue[soundfile]["Left"]))
  171. coNode.setAttribute("right", str(queue[soundfile].has_key("Right") and queue[soundfile]["Right"]))
  172. sfNode.appendChild(coNode)
  173. root.documentElement.appendChild(sfNode)
  174. numFilesProcessed = numFilesProcessed + 1
  175. progress(50, float(numFilesProcessed)/float(numFiles) * 100)
  176. root.writexml(xmlfile, addindent="\t", newl="\n", encoding="utf-8")
  177. ## XML Queue Reader
  178. def readQueueFromXML(infile):
  179. print("Reading queue from xml: {0}".format(infile))
  180. queueXML = minidom.parse(infile)
  181. soundfiles = queueXML.getElementsByTagName("soundfile")
  182. numFilesProcessed = 0
  183. numFiles = len(soundfiles)
  184. progress(50, float(numFilesProcessed)/float(numFiles) * 100)
  185. for soundfile in soundfiles:
  186. channelOptions = {}
  187. channelXML = soundfile.getElementsByTagName("channeloutputs")
  188. for channelNode in channelXML:
  189. if "stereo" in channelNode.attributes.keys() and channelNode.attributes["stereo"].value == "True":
  190. channelOptions["Both"] = True
  191. if "left" in channelNode.attributes.keys() and channelNode.attributes["left"].value == "True":
  192. channelOptions["Left"] = True
  193. if "right" in channelNode.attributes.keys() and channelNode.attributes["right"].value == "True":
  194. channelOptions["Right"] = True
  195. queue[soundfile.attributes["name"].value] = channelOptions
  196. numFilesProcessed = numFilesProcessed + 1
  197. progress(50, float(numFilesProcessed)/float(numFiles) * 100)
  198. print("{0} sound files added to queue.".format(len(queue)))
  199. return True
  200. ## Handy Progress Meter
  201. def progress(width, percent):
  202. marks = math.floor(width * (percent / 100.0))
  203. spaces = math.floor(width - marks)
  204. loader = '[ ' + ('=' * int(marks)) + (' ' * int(spaces)) + ' ]'
  205. sys.stdout.write("%s %d%%\r" % (loader, percent))
  206. if percent >= 100:
  207. sys.stdout.write("\n")
  208. sys.stdout.flush()
  209. if __name__ == '__main__':
  210. parser = OptionParser(usage="usage: %prog [options]", version="%prog {0}".format(version))
  211. parser.add_option("-q", "--quiet", dest="verbose", default=True, action="store_false", help="Don't print status messages")
  212. pathgroup = OptionGroup(parser, "Path Output Options")
  213. pathgroup.add_option("-u", "--uru", dest="urudir", default=DefaultUruDir, help="Sets base path for Uru.")
  214. pathgroup.add_option("-d", "--data", dest="datadir", help="Override path for input data files.")
  215. pathgroup.add_option("-s", "--sfx", dest="sfxdir", help="Override path for input sound files.")
  216. pathgroup.add_option("-c", "--cache", dest="cachedir", help="Override path for output sound files.")
  217. parser.add_option_group(pathgroup)
  218. xmlgroup = OptionGroup(parser, "XML Output Options", "")
  219. xmlgroup.add_option("--xml", dest="xml", default=False, action="store_true", help="Dump queue to XML after Age processing and audio conversion.")
  220. xmlgroup.add_option("--xmlonly", dest="xmlonly", default=False, action="store_true", help="Don't convert audio after Age processing, only dump XML.")
  221. xmlgroup.add_option("--xmlof", dest="xmloutfile", metavar="OUTFILE", default="wavlist.xml", help="File to dump queue to after processing if --xml or --xmlonly option is specified.")
  222. xmlgroup.add_option("--xmlin", dest="xmlinfile", metavar="INFILE", help="Use specified XML file instead of local Age files, then do audio conversion.")
  223. parser.add_option_group(xmlgroup)
  224. (options, args) = parser.parse_args()
  225. ## Send output to OS's null if unwanted
  226. if not options.verbose:
  227. sys.stdout = open(os.devnull,"w")
  228. sys.stderr = open(os.devnull,"w")
  229. ## Compute Paths
  230. basedir = os.path.expanduser(options.urudir)
  231. datadir = os.path.join(basedir, DefaultDataDir)
  232. sfxdir = os.path.join(basedir, DefaultSFXDir)
  233. cachedir = os.path.join(basedir, DefaultCacheDir)
  234. if options.datadir: datadir = os.path.expanduser(options.datadir)
  235. if options.sfxdir: sfxdir = os.path.expanduser(options.sfxdir)
  236. if options.cachedir: cachedir = os.path.expanduser(options.cachedir)
  237. ## Do the work!
  238. if not options.xmlinfile:
  239. if getDecompressQueue(datadir):
  240. if not options.xmlonly:
  241. doDecompress(sfxdir, cachedir)
  242. if options.xml or options.xmlonly:
  243. writeQueueToXML(os.path.expanduser(options.xmloutfile))
  244. elif readQueueFromXML(os.path.expanduser(options.xmlinfile)):
  245. doDecompress(sfxdir, cachedir)