Module Editor
[hide private]
[frames] | no frames]

Source Code for Module Editor

  1  ##################################################################### 
  2  # -*- coding: iso-8859-1 -*-                                        # 
  3  #                                                                   # 
  4  # Frets on Fire                                                     # 
  5  # Copyright (C) 2006 Sami Kyöstilä                                  # 
  6  #                                                                   # 
  7  # This program is free software; you can redistribute it and/or     # 
  8  # modify it under the terms of the GNU General Public License       # 
  9  # as published by the Free Software Foundation; either version 2    # 
 10  # of the License, or (at your option) any later version.            # 
 11  #                                                                   # 
 12  # This program is distributed in the hope that it will be useful,   # 
 13  # but WITHOUT ANY WARRANTY; without even the implied warranty of    # 
 14  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the     # 
 15  # GNU General Public License for more details.                      # 
 16  #                                                                   # 
 17  # You should have received a copy of the GNU General Public License # 
 18  # along with this program; if not, write to the Free Software       # 
 19  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,        # 
 20  # MA  02110-1301, USA.                                              # 
 21  ##################################################################### 
 22   
 23  import pygame 
 24  from OpenGL.GL import * 
 25  from OpenGL.GLU import * 
 26  import math 
 27  import colorsys 
 28   
 29  from View import Layer 
 30  from Input import KeyListener 
 31  from Song import loadSong, createSong, Note, difficulties, DEFAULT_LIBRARY 
 32  from Guitar import Guitar, KEYS 
 33  from Camera import Camera 
 34  from Menu import Menu, Choice 
 35  from Language import _ 
 36  import MainMenu 
 37  import Dialogs 
 38  import Player 
 39  import Theme 
 40  import Log 
 41  import shutil, os, struct, wave, tempfile 
 42  from struct import unpack 
 43   
44 -class Editor(Layer, KeyListener):
45 """Song editor layer."""
46 - def __init__(self, engine, songName = None, libraryName = DEFAULT_LIBRARY):
47 self.engine = engine 48 self.time = 0.0 49 self.guitar = Guitar(self.engine, editorMode = True) 50 self.controls = Player.Controls() 51 self.camera = Camera() 52 self.pos = 0.0 53 self.snapPos = 0.0 54 self.scrollPos = 0.0 55 self.scrollSpeed = 0.0 56 self.newNotes = None 57 self.newNotePos = 0.0 58 self.song = None 59 self.engine.loadSvgDrawing(self, "background", "editor.svg") 60 self.modified = False 61 self.songName = songName 62 self.libraryName = libraryName 63 self.heldFrets = set() 64 65 mainMenu = [ 66 (_("Save Song"), self.save), 67 (_("Set Song Name"), self.setSongName), 68 (_("Set Artist Name"), self.setArtistName), 69 (_("Set Beats per Minute"), self.setBpm), 70 (_("Estimate Beats per Minute"), self.estimateBpm), 71 (_("Set A/V delay"), self.setAVDelay), 72 (_("Set Cassette Color"), self.setCassetteColor), 73 (_("Set Cassette Label"), self.setCassetteLabel), 74 (_("Editing Help"), self.help), 75 (_("Quit to Main Menu"), self.quit), 76 ] 77 self.menu = Menu(self.engine, mainMenu)
78
79 - def save(self):
80 if not self.modified: 81 Dialogs.showMessage(self.engine, _("There are no changes to save.")) 82 return 83 84 def save(): 85 self.song.save() 86 self.modified = False
87 88 self.engine.resource.load(function = save) 89 Dialogs.showLoadingScreen(self.engine, lambda: not self.modified, text = _("Saving...")) 90 Dialogs.showMessage(self.engine, _("'%s' saved.") % self.song.info.name)
91
92 - def help(self):
93 Dialogs.showMessage(self.engine, _("Editing keys: ") + 94 _("Arrows - Move cursor, ") + 95 _("Space - Play/pause song, ") + 96 _("Enter - Make note (hold and move for long notes), ") + 97 _("Delete - Delete note, ") + 98 _("Page Up/Down - Change difficulty"))
99 100
101 - def setSongName(self):
102 name = Dialogs.getText(self.engine, _("Enter Song Name"), self.song.info.name) 103 if name: 104 self.song.info.name = name 105 self.modified = True
106
107 - def setArtistName(self):
108 name = Dialogs.getText(self.engine, _("Enter Artist Name"), self.song.info.artist) 109 if name: 110 self.song.info.artist = name 111 self.modified = True
112
113 - def setAVDelay(self):
114 delay = Dialogs.getText(self.engine, _("Enter A/V delay in milliseconds"), unicode(self.song.info.delay)) 115 if delay: 116 try: 117 self.song.info.delay = int(delay) 118 self.modified = True 119 except ValueError: 120 Dialogs.showMessage(self.engine, _("That isn't a number."))
121
122 - def setBpm(self):
123 bpm = Dialogs.getText(self.engine, _("Enter Beats per Minute Value"), unicode(self.song.bpm)) 124 if bpm: 125 try: 126 self.song.setBpm(float(bpm)) 127 self.modified = True 128 except ValueError: 129 Dialogs.showMessage(self.engine, _("That isn't a number."))
130
131 - def estimateBpm(self):
132 bpm = Dialogs.estimateBpm(self.engine, self.song, _("Tap the Space bar to the beat of the song. Press Enter when done or Escape to cancel.")) 133 if bpm is not None: 134 self.song.setBpm(bpm) 135 self.modified = True
136
137 - def setCassetteColor(self):
138 if self.song.info.cassetteColor: 139 color = Theme.colorToHex(self.song.info.cassetteColor) 140 else: 141 color = "" 142 color = Dialogs.getText(self.engine, _("Enter cassette color in HTML (#RRGGBB) format."), color) 143 if color: 144 try: 145 self.song.info.setCassetteColor(Theme.hexToColor(color)) 146 self.modified = True 147 except ValueError: 148 Dialogs.showMessage(self.engine, _("That isn't a color."))
149
150 - def setCassetteLabel(self):
151 label = Dialogs.chooseFile(self.engine, masks = ["*.png"], prompt = _("Choose a 256x128 PNG format label image.")) 152 if label: 153 songPath = self.engine.resource.fileName("songs", self.songName, writable = True) 154 shutil.copyfile(label, os.path.join(songPath, "label.png")) 155 self.modified = True
156
157 - def shown(self):
158 self.engine.input.addKeyListener(self) 159 160 if not self.songName: 161 self.libraryName, self.songName = Dialogs.chooseSong(self.engine) 162 163 if not self.songName: 164 self.engine.view.popLayer(self) 165 return 166 167 self.engine.resource.load(self, "song", lambda: loadSong(self.engine, self.songName, seekable = True, library = self.libraryName)) 168 Dialogs.showLoadingScreen(self.engine, lambda: self.song, text = _("Loading song..."))
169
170 - def hidden(self):
171 if self.song: 172 self.song.stop() 173 self.engine.input.removeKeyListener(self) 174 self.engine.view.pushLayer(MainMenu.MainMenu(self.engine))
175
176 - def controlPressed(self, control):
177 if not self.song: 178 return 179 180 if control == Player.UP: 181 self.guitar.selectPreviousString() 182 elif control == Player.DOWN: 183 self.guitar.selectNextString() 184 elif control == Player.LEFT: 185 self.pos = self.snapPos - self.song.period / 4 186 elif control == Player.RIGHT: 187 self.pos = self.snapPos + self.song.period / 4 188 elif control in KEYS: 189 self.heldFrets.add(KEYS.index(control)) 190 elif control in [Player.ACTION1, Player.ACTION2]: 191 self.newNotePos = self.snapPos 192 # Add notes for the frets that are held down or for the selected string. 193 if self.heldFrets: 194 self.newNotes = [Note(f, self.song.period / 4) for f in self.heldFrets] 195 else: 196 self.newNotes = [Note(self.guitar.selectedString, self.song.period / 4)] 197 self.modified = True
198
199 - def controlReleased(self, control):
200 if not self.song: 201 return 202 203 if control in [Player.ACTION1, Player.ACTION2] and self.newNotes and not self.heldFrets: 204 self.newNotes = [] 205 elif control in KEYS: 206 self.heldFrets.remove(KEYS.index(control)) 207 if not self.heldFrets and self.newNotes: 208 self.newNotes = []
209
210 - def quit(self):
211 self.engine.view.popLayer(self) 212 self.engine.view.popLayer(self.menu)
213
214 - def keyPressed(self, key, unicode):
215 c = self.engine.input.controls.getMapping(key) 216 if c == Player.CANCEL: 217 self.engine.view.pushLayer(self.menu) 218 elif key == pygame.K_PAGEDOWN and self.song: 219 d = self.song.difficulty 220 v = difficulties.values() 221 self.song.difficulty = v[(v.index(d) + 1) % len(v)] 222 elif key == pygame.K_PAGEUP and self.song: 223 d = self.song.difficulty 224 v = difficulties.values() 225 self.song.difficulty = v[(v.index(d) - 1) % len(v)] 226 elif key == pygame.K_DELETE and self.song: 227 # gather up all events that intersect the cursor and delete the ones on the selected string 228 t1 = self.snapPos 229 t2 = self.snapPos + self.song.period / 4 230 e = [(time, event) for time, event in self.song.track.getEvents(t1, t2) if isinstance(event, Note)] 231 for time, event in e: 232 if event.number == self.guitar.selectedString: 233 self.song.track.removeEvent(time, event) 234 self.modified = True 235 elif key == pygame.K_SPACE and self.song: 236 if self.song.isPlaying(): 237 self.song.stop() 238 else: 239 self.song.play(start = self.pos) 240 c = self.controls.keyPressed(key) 241 if c: 242 self.controlPressed(c) 243 return True
244
245 - def keyReleased(self, key):
246 c = self.controls.keyReleased(key) 247 if c: 248 self.controlReleased(c) 249 return True
250
251 - def run(self, ticks):
252 self.time += ticks / 50.0 253 254 if not self.song: 255 return 256 257 self.guitar.run(ticks, self.scrollPos, self.controls) 258 259 if not self.song.isPlaying(): 260 if self.controls.getState(Player.RIGHT) and not self.controls.getState(Player.LEFT): 261 self.pos += self.song.period * self.scrollSpeed 262 self.scrollSpeed += ticks / 4096.0 263 elif self.controls.getState(Player.LEFT) and not self.controls.getState(Player.RIGHT): 264 self.pos -= self.song.period * self.scrollSpeed 265 self.scrollSpeed += ticks / 4096.0 266 else: 267 self.scrollSpeed = 0 268 else: 269 self.pos = self.song.getPosition() - self.song.info.delay 270 271 self.pos = max(0, self.pos) 272 273 quarterBeat = int(self.pos / (self.song.period / 4) + .5) 274 self.snapPos = quarterBeat * (self.song.period / 4) 275 276 # note adding 277 if self.newNotes: 278 if self.snapPos < self.newNotePos: 279 self.newNotes = [] 280 for note in self.newNotes: 281 self.song.track.removeEvent(self.newNotePos, note) 282 note.length = max(self.song.period / 4, self.snapPos - self.newNotePos) 283 # remove all notes under the this new note 284 oldNotes = [(time, event) for time, event in self.song.track.getEvents(self.newNotePos, self.newNotePos + note.length) if isinstance(event, Note)] 285 for time, event in oldNotes: 286 if event.number == note.number: 287 self.song.track.removeEvent(time, event) 288 if time < self.newNotePos: 289 event.length = self.newNotePos - time 290 self.song.track.addEvent(time, event) 291 self.song.track.addEvent(self.newNotePos, note) 292 293 if self.song.isPlaying(): 294 self.scrollPos = self.pos 295 else: 296 self.scrollPos = (self.scrollPos + self.snapPos) / 2.0
297
298 - def render(self, visibility, topMost):
299 if not self.song: 300 return 301 302 v = 1.0 - ((1 - visibility) ** 2) 303 304 # render the background 305 t = self.time / 100 + 34 306 w, h, = self.engine.view.geometry[2:4] 307 r = .5 308 self.background.transform.reset() 309 self.background.transform.translate(w / 2 + math.sin(t / 2) * w / 2 * r, h / 2 + math.cos(t) * h / 2 * r) 310 self.background.transform.rotate(-t) 311 self.background.transform.scale(math.sin(t / 8) + 2, math.sin(t / 8) + 2) 312 self.background.draw() 313 314 self.camera.target = ( 2, 0, 5.5) 315 self.camera.origin = (-2, 9, 5.5) 316 317 glMatrixMode(GL_PROJECTION) 318 glLoadIdentity() 319 gluPerspective(60, 4.0 / 3.0, 0.1, 1000) 320 glMatrixMode(GL_MODELVIEW) 321 glLoadIdentity() 322 self.camera.apply() 323 self.guitar.render(v, self.song, self.scrollPos, self.controls) 324 325 self.engine.view.setOrthogonalProjection(normalize = True) 326 font = self.engine.data.font 327 328 try: 329 Theme.setSelectedColor() 330 331 w, h = font.getStringSize(" ") 332 333 if self.song.isPlaying(): 334 status = _("Playing") 335 else: 336 status = _("Stopped") 337 338 t = "%d.%02d'%03d" % (self.pos / 60000, (self.pos % 60000) / 1000, self.pos % 1000) 339 font.render(t, (.05, .05 - h / 2)) 340 font.render(status, (.05, .05 + h / 2)) 341 font.render(unicode(self.song.difficulty), (.05, .05 + 3 * h / 2)) 342 343 Theme.setBaseColor() 344 text = self.song.info.name + (self.modified and "*" or "") 345 Dialogs.wrapText(font, (.5, .05 - h / 2), text) 346 finally: 347 self.engine.view.resetProjection()
348
349 -class Importer(Layer):
350 """ 351 Song importer. 352 353 This importer needs two OGG tracks for the new song; one is the background track 354 and the other is the guitar track. The importer will create a blank note and info files 355 and copy the tracks under the data directory. 356 """
357 - def __init__(self, engine):
358 self.engine = engine 359 self.wizardStarted = False 360 self.song = None 361 self.songName = None
362
363 - def hidden(self):
364 if self.songName: 365 self.engine.view.pushLayer(Editor(self.engine, self.songName)) 366 else: 367 self.engine.view.pushLayer(MainMenu.MainMenu(self.engine))
368
369 - def run(self, ticks):
370 if self.wizardStarted: 371 return 372 self.wizardStarted = True 373 374 name = "" 375 while True: 376 masks = ["*.ogg"] 377 name = Dialogs.getText(self.engine, prompt = _("Enter a name for the song."), text = name) 378 379 if not name: 380 self.engine.view.popLayer(self) 381 return 382 383 path = os.path.abspath(self.engine.resource.fileName("songs", name)) 384 if os.path.isdir(path): 385 Dialogs.showMessage(self.engine, _("That song already exists.")) 386 else: 387 break 388 389 guitarTrack = Dialogs.chooseFile(self.engine, masks = masks, prompt = _("Choose the Instrument Track (OGG format).")) 390 391 if not guitarTrack: 392 self.engine.view.popLayer(self) 393 return 394 395 backgroundTrack = Dialogs.chooseFile(self.engine, masks = masks, prompt = _("Choose the Background Track (OGG format) or press Escape to skip.")) 396 397 # Create the song 398 loader = self.engine.resource.load(self, "song", lambda: createSong(self.engine, name, guitarTrack, backgroundTrack)) 399 Dialogs.showLoadingScreen(self.engine, lambda: self.song or loader.exception, text = _("Importing...")) 400 401 if not loader.exception: 402 self.songName = name 403 self.engine.view.popLayer(self)
404
405 -class ArkFile(object):
406 """ 407 An interface to the ARK file format of Guitar Hero. 408 409 The format of the archive and the index file was studied from 410 Game Extractor by WATTO Studios. 411 """
412 - def __init__(self, indexFileName, dataFileName):
413 self.dataFileName = dataFileName 414 415 # Read the available files from the index 416 f = open(indexFileName, "rb") 417 magic, version1, version2, arkSize, length = unpack("IIIII", f.read(5 * 4)) 418 419 Log.debug("Reading HDR file v%d.%d. Main archive is %d bytes." % (version1, version2, arkSize)) 420 421 # Read the name array 422 fileNameData = f.read(length) 423 fileNameCount, = unpack("I", f.read(4)) 424 fileNameOffsets = [unpack("I", f.read(4))[0] for i in range(fileNameCount)] 425 426 # Read the pointers to the names 427 names = [] 428 for i, offset in enumerate(fileNameOffsets): 429 length = fileNameData[offset:].find("\x00") 430 fileName = fileNameData[offset:offset + length] 431 names.append(fileName) 432 433 # Read the file names themselves 434 fileCount, = unpack("I", f.read(4)) 435 436 self.files = {} 437 for i in range(fileCount): 438 offset, fileIndex, dirIndex, length, null = unpack("IIIII", f.read(5 * 4)) 439 fullName = "%s/%s" % (names[dirIndex], names[fileIndex]) 440 self.files[fullName] = offset, length 441 Log.debug("File: %s at offset %d, length %d bytes." % (fullName, offset, length)) 442 Log.debug("Archive contains %d files." % len(self.files)) 443 f.close()
444
445 - def openFile(self, name, mode = "rb"):
446 offset, length = self.files[name] 447 f = open(self.dataFileName, mode) 448 f.seek(offset) 449 return f
450
451 - def fileLength(self, name):
452 offset, length = self.files[name] 453 return length
454
455 - def extractFile(self, name, outputFile):
456 f = self.openFile(name) 457 length = self.fileLength(name) 458 459 if type(outputFile) == str: 460 out = open(outputFile, "wb") 461 else: 462 out = outputFile 463 464 while length > 0: 465 data = f.read(4096) 466 data = data[:min(length, len(data))] 467 length -= len(data) 468 out.write(data) 469 f.close() 470 471 if type(outputFile) == str: 472 out.close()
473
474 -class GHImporter(Layer):
475 """ 476 Guitar Hero(tm) song importer. 477 478 This importer takes the original Guitar Hero PS2 DVD and extracts the songs from it. 479 Thanks to Sami Vaarala for the initial implementation! 480 """
481 - def __init__(self, engine):
482 self.engine = engine 483 self.wizardStarted = False 484 self.done = False 485 self.songs = None 486 self.statusText = "" 487 self.stageInfoText = "" 488 self.stageProgress = 0.0
489
490 - def hidden(self):
491 self.engine.boostBackgroundThreads(False) 492 self.engine.view.pushLayer(MainMenu.MainMenu(self.engine))
493
494 - def decodeVgsStreams(self, vgsFile, length):
495 Log.notice("Decompressing %d byte VGS file." % (length)) 496 497 # This decompressor is based on VAG-Depack by bITmASTER 498 f = vgsFile 499 500 c = [[ 0.0, 0.0 ], 501 [ 60.0 / 64.0, 0.0 ], 502 [ 115.0 / 64.0, -52.0 / 64.0 ], 503 [ 98.0 / 64.0, -55.0 / 64.0 ], 504 [ 122.0 / 64.0, -60.0 / 64.0 ], 505 [ 0.0, 0.0 ], 506 [ 0.0, 0.0 ], 507 [ 0.0, 0.0 ]] 508 509 class StreamDecoder: 510 """XA ADPCM decoder""" 511 def __init__(self): 512 self.s_1 = 0.0 513 self.s_2 = 0.0 514 self.samples = [0.0] * 28
515 516 def decode(self, block, predict_nr, shift_factor): 517 for i in range(0, 28, 2): 518 d = block[i >> 1] 519 s = (d & 0xf) << 12 520 if s & 0x8000: 521 s = (s & 0xffff) - 0x10000 522 self.samples[i] = float(s >> shift_factor) 523 s = (d & 0xf0) << 8 524 if s & 0x8000: 525 s = (s & 0xffff) - 0x10000 526 self.samples[i + 1] = float(s >> shift_factor) 527 528 for i in range(28): 529 self.samples[i] += self.s_1 * c[predict_nr][0] + self.s_2 * c[predict_nr][1] 530 self.s_2 = self.s_1 531 self.s_1 = self.samples[i] 532 533 return self.samples
534 535 maxStreams = 8 536 streams = [StreamDecoder() for i in range(maxStreams)] 537 538 def byte(d): 539 if len(d): 540 return struct.unpack("b", d)[0] 541 return 0 542 543 startPos = f.tell() 544 545 while True: 546 self.stageProgress = float(f.tell() - startPos) / length 547 548 d = f.read(1) 549 if not d: 550 break 551 predict_nr = byte(d) 552 shift_factor = predict_nr & 0xf 553 predict_nr >>= 4 554 flags = byte(f.read(1)) 555 556 if flags == 7 or flags < 0: 557 break 558 559 streamId = flags % maxStreams 560 block = [byte(f.read(1)) for i in range(14)] 561 samples = streams[streamId].decode(block, predict_nr, shift_factor) 562 yield (streamId, struct.pack("28h", *[max(min(int(d + .5), 32767), -32768) for d in samples])) 563 564 f.close() 565
566 - def joinPcmFiles(self, pcmLeft, pcmRight, waveOut, sampleRate = 44100):
567 pcmLeft = open(pcmLeft, "rb") 568 pcmRight = open(pcmRight, "rb") 569 w = wave.open(waveOut, "w") 570 w.setnchannels(2) 571 w.setsampwidth(2) 572 w.setframerate(sampleRate) 573 574 pcmLeft.seek(0, 2) 575 length = pcmLeft.tell() 576 pcmLeft.seek(0) 577 578 while length: 579 self.stageProgress = float(pcmLeft.tell()) / length 580 l = pcmLeft.read(2) 581 r = pcmRight.read(2) 582 if not l or not r: 583 break 584 w.writeframesraw(l + r) 585 w.close() 586 pcmLeft.close() 587 pcmRight.close()
588
589 - def decodeVgsFile(self, vgsFile, length, outputSongOggFile, outputGuitarOggFile, outputRhythmOggFile, workPath):
590 # Split the file into different tracks 591 self.stageInfoText = _("Stage 1/8: Splitting VGS file") 592 593 # For debugging 594 #os.system("cp /tmp/foo.ogg " + outputSongOggFile) 595 #os.system("cp /tmp/foo.ogg " + outputGuitarOggFile) 596 #os.system("cp /tmp/foo.ogg " + outputRhythmOggFile) 597 #return 598 599 f = vgsFile 600 blockSize = 16 601 602 header = f.read(0x80) 603 magic, version = unpack("4si", header[:4 + 4]) 604 assert magic == "VgS!" 605 header = header[4 + 4:] 606 607 Log.debug("VGS version %d" % (version)) 608 609 streams = [] 610 for channels in range(16): 611 rate, blocks = unpack("ii", header[:4 + 4]) 612 header = header[4 + 4:] 613 if not rate or not blocks: 614 break 615 Log.debug("Stream %d: %d blocks at %d Hz" % (len(streams), blocks, rate)) 616 streams.append((rate, blocks)) 617 618 out = [open(os.path.join(workPath, "chan%d.pcm") % c, "wb") for c in range(channels)] 619 620 for channel, data in self.decodeVgsStreams(vgsFile, length): 621 if channel >= 0 and channel < len(out): 622 out[channel].write(data) 623 624 [o.close() for o in out] 625 626 # Join the left and right tracks to stereo wave files 627 Log.notice("Joining song and guitar tracks") 628 songWaveFile = os.path.join(workPath, "song.wav") 629 guitarWaveFile = os.path.join(workPath, "guitar.wav") 630 rhythmWaveFile = os.path.join(workPath, "rhythm.wav") 631 632 self.stageInfoText = _("Stage 6/8: Joining song stereo tracks") 633 self.joinPcmFiles(os.path.join(workPath, "chan0.pcm"), 634 os.path.join(workPath, "chan1.pcm"), 635 songWaveFile, sampleRate = streams[0][0]) 636 self.stageInfoText = _("Stage 7/8: Joining guitar stereo tracks") 637 self.joinPcmFiles(os.path.join(workPath, "chan2.pcm"), 638 os.path.join(workPath, "chan3.pcm"), 639 guitarWaveFile, sampleRate = streams[2][0]) 640 641 if channels == 5: 642 self.joinPcmFiles(os.path.join(workPath, "chan4.pcm"), 643 os.path.join(workPath, "chan4.pcm"), 644 rhythmWaveFile, sampleRate = streams[4][0]) 645 elif channels == 6: 646 self.joinPcmFiles(os.path.join(workPath, "chan4.pcm"), 647 os.path.join(workPath, "chan5.pcm"), 648 rhythmWaveFile, sampleRate = streams[4][0]) 649 650 # Compress wave files 651 self.stageInfoText = _("Stage 8/8: Compressing tracks") 652 self.stageProgress = 0.0 / channels 653 Log.notice("Compressing song and guitar tracks") 654 self.compressWaveFileToOgg(songWaveFile, outputSongOggFile) 655 self.stageProgress = 2.0 / channels 656 self.compressWaveFileToOgg(guitarWaveFile, outputGuitarOggFile) 657 self.stageProgress = 4.0 / channels 658 if channels in [5, 6]: 659 self.compressWaveFileToOgg(rhythmWaveFile, outputRhythmOggFile) 660 self.stageProgress = 6.0 / channels 661 662 # Cleanup 663 for chan in range(channels): 664 os.unlink(os.path.join(workPath, "chan%d.pcm" % chan)) 665 os.unlink(songWaveFile) 666 os.unlink(guitarWaveFile) 667 if channels in [5, 6]: 668 os.unlink(rhythmWaveFile)
669
670 - def compressWaveFileToOgg(self, waveFile, oggFile):
671 os.system('oggenc "%s" --resample 44100 -q 6 -o "%s"' % (waveFile, oggFile))
672
673 - def isOggEncoderPresent(self):
674 if os.name == "nt": 675 return os.system("oggenc -h > NUL:") == 0 676 return os.system("oggenc > /dev/null 2>&1") == 256
677
678 - def importSongs(self, headerPath, archivePath, workPath):
679 try: 680 try: 681 os.mkdir(workPath) 682 except: 683 pass 684 685 # Read the song map 686 self.statusText = _("Reading the song list.") 687 songMap = {} 688 vgsMap = {} 689 library = DEFAULT_LIBRARY 690 for line in open(self.engine.resource.fileName("ghmidimap.txt")): 691 fields = map(lambda s: s.strip(), line.strip().split(";")) 692 if fields[0] == "$library": 693 library = os.path.join(DEFAULT_LIBRARY, fields[1]) 694 else: 695 songName, fullName, artist = fields 696 songMap[songName] = (library, fullName, artist) 697 698 self.statusText = _("Reading the archive index.") 699 archive = ArkFile(headerPath, archivePath) 700 songs = [] 701 702 # Filter out the songs that aren't in this archive 703 for songName, data in songMap.items(): 704 library, fullName, artist = data 705 songPath = self.engine.resource.fileName(library, songName, writable = True) 706 707 vgsMap[songName] = "songs/%s/%s.vgs" % (songName, songName) 708 if not vgsMap[songName] in archive.files: 709 vgsMap[songName] = "songs/%s/%s_sp.vgs" % (songName, songName) 710 if not vgsMap[songName] in archive.files: 711 Log.warn("VGS file for song '%s' not found in this archive." % songName) 712 del songMap[songName] 713 continue 714 715 if os.path.exists(songPath): 716 Log.warn("Song '%s' already exists." % songName) 717 del songMap[songName] 718 continue 719 720 for songName, data in songMap.items(): 721 library, fullName, artist = data 722 songPath = self.engine.resource.fileName(library, songName, writable = True) 723 print songPath 724 725 Log.notice("Extracting song '%s'" % songName) 726 self.statusText = _("Extracting %s by %s. %d of %d songs imported. Yeah, this is going to take forever.") % (fullName, artist, len(songs), len(songMap)) 727 728 archiveMidiFile = "songs/%s/%s.mid" % (songName, songName) 729 archiveVgsFile = vgsMap[songName] 730 731 # Check that the required files exist 732 if not archiveMidiFile in archive.files: 733 Log.warn("MIDI file for song '%s' not found." % songName) 734 continue 735 736 if not archiveVgsFile in archive.files: 737 Log.warn("VGS file for song '%s' not found." % songName) 738 continue 739 740 # Debug dump 741 #vgsFile = archive.openFile(archiveVgsFile) 742 #open("/tmp/test.vgs", "wb").write(vgsFile.read(archive.fileLength(archiveVgsFile))) 743 744 # Grab the VGS file 745 vgsFile = archive.openFile(archiveVgsFile) 746 vgsFileLength = archive.fileLength(archiveVgsFile) 747 guitarOggFile = os.path.join(workPath, "guitar.ogg") 748 songOggFile = os.path.join(workPath, "song.ogg") 749 rhythmOggFile = os.path.join(workPath, "rhythm.ogg") 750 self.decodeVgsFile(vgsFile, vgsFileLength, songOggFile, guitarOggFile, rhythmOggFile, workPath) 751 vgsFile.close() 752 753 # Create the song 754 if not os.path.isfile(rhythmOggFile): 755 rhythmOggFile = None 756 757 song = createSong(self.engine, songName, guitarOggFile, songOggFile, rhythmOggFile, library = library) 758 song.info.name = fullName.strip() 759 song.info.artist = artist.strip() 760 song.save() 761 762 # Grab the MIDI file 763 archive.extractFile(archiveMidiFile, os.path.join(songPath, "notes.mid")) 764 765 # Done with this song 766 songs.append(songName) 767 768 # Clean up 769 shutil.rmtree(workPath) 770 771 self.stageInfoText = _("Ready") 772 self.stageProgress = 1.0 773 Log.debug("Songs imported: " + ", ".join(songs)) 774 return songs 775 except: 776 self.done = True 777 raise
778
779 - def run(self, ticks):
780 if self.done == True or self.songs is not None: 781 songs = self.songs 782 self.songs = None 783 self.done = None 784 self.statusText = "" 785 if songs: 786 Dialogs.showMessage(self.engine, _("All done! %d songs imported. Have fun!") % (len(songs))) 787 else: 788 Dialogs.showMessage(self.engine, _("No songs could be imported, sorry. Check the log files for more information.")) 789 self.engine.view.popLayer(self) 790 if self.wizardStarted: 791 return 792 self.wizardStarted = True 793 794 # Check for necessary software 795 if not self.isOggEncoderPresent(): 796 if os.name == "nt": 797 Dialogs.showMessage(self.engine, _("Ogg Vorbis encoder (oggenc.exe) not found. Please install it to the game directory and try again.")) 798 else: 799 Dialogs.showMessage(self.engine, _("Ogg Vorbis encoder (oggenc) not found. Please install it and try again.")) 800 self.engine.view.popLayer(self) 801 return 802 803 Dialogs.showMessage(self.engine, _("Make sure you have at least 500 megabytes of free disk space before using this importer.")) 804 805 path = "" 806 while True: 807 path = Dialogs.getText(self.engine, prompt = _("Enter the path to the mounted Guitar Hero (tm) I/II/Encore DVD"), text = path) 808 809 if not path: 810 self.engine.view.popLayer(self) 811 return 812 813 headerPath = os.path.join(path, "gen", "main.hdr") 814 archivePath = os.path.join(path, "gen", "main_0.ark") 815 if not os.path.isfile(headerPath) or not os.path.isfile(archivePath): 816 Dialogs.showMessage(self.engine, _("That's not it. Try again.")) 817 else: 818 break 819 820 workPath = tempfile.mkdtemp("fretsonfire") 821 self.engine.boostBackgroundThreads(True) 822 self.engine.resource.load(self, "songs", lambda: self.importSongs(headerPath, archivePath, workPath))
823
824 - def render(self, visibility, topMost):
825 v = (1 - visibility) ** 2 826 827 self.engine.view.setOrthogonalProjection(normalize = True) 828 font = self.engine.data.font 829 830 try: 831 w, h = font.getStringSize(" ") 832 833 Theme.setSelectedColor() 834 font.render(_("Importing Songs"), (.05, .05 - v)) 835 if self.stageInfoText: 836 font.render("%s (%d%%)" % (self.stageInfoText, 100 * self.stageProgress), (.05, .7 + v), scale = 0.001) 837 838 Theme.setBaseColor() 839 Dialogs.wrapText(font, (.1, .3 + v), self.statusText) 840 finally: 841 self.engine.view.resetProjection()
842