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

Source Code for Module Song

  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 midi 
 24  import Log 
 25  import Audio 
 26  from ConfigParser import ConfigParser 
 27  import os 
 28  import re 
 29  import shutil 
 30  import Config 
 31  import sha 
 32  import binascii 
 33  import Cerealizer 
 34  import urllib 
 35  import Version 
 36  import Theme 
 37  from Language import _ 
 38   
 39  DEFAULT_LIBRARY         = "songs" 
 40   
 41  AMAZING_DIFFICULTY      = 0 
 42  MEDIUM_DIFFICULTY       = 1 
 43  EASY_DIFFICULTY         = 2 
 44  SUPAEASY_DIFFICULTY     = 3 
 45   
46 -class Difficulty:
47 - def __init__(self, id, text):
48 self.id = id 49 self.text = text
50
51 - def __str__(self):
52 return self.text
53
54 - def __repr__(self):
55 return self.text
56 57 difficulties = { 58 SUPAEASY_DIFFICULTY: Difficulty(SUPAEASY_DIFFICULTY, _("Supaeasy")), 59 EASY_DIFFICULTY: Difficulty(EASY_DIFFICULTY, _("Easy")), 60 MEDIUM_DIFFICULTY: Difficulty(MEDIUM_DIFFICULTY, _("Medium")), 61 AMAZING_DIFFICULTY: Difficulty(AMAZING_DIFFICULTY, _("Amazing")), 62 } 63
64 -class SongInfo(object):
65 - def __init__(self, infoFileName):
66 self.songName = os.path.basename(os.path.dirname(infoFileName)) 67 self.fileName = infoFileName 68 self.info = ConfigParser() 69 self._difficulties = None 70 71 try: 72 self.info.read(infoFileName) 73 except: 74 pass 75 76 # Read highscores and verify their hashes. 77 # There ain't no security like security throught obscurity :) 78 self.highScores = {} 79 80 scores = self._get("scores", str, "") 81 if scores: 82 scores = Cerealizer.loads(binascii.unhexlify(scores)) 83 for difficulty in scores.keys(): 84 try: 85 difficulty = difficulties[difficulty] 86 except KeyError: 87 continue 88 for score, stars, name, hash in scores[difficulty.id]: 89 if self.getScoreHash(difficulty, score, stars, name) == hash: 90 self.addHighscore(difficulty, score, stars, name) 91 else: 92 Log.warn("Weak hack attempt detected. Better luck next time.")
93
94 - def _set(self, attr, value):
95 if not self.info.has_section("song"): 96 self.info.add_section("song") 97 if type(value) == unicode: 98 value = value.encode(Config.encoding) 99 else: 100 value = str(value) 101 self.info.set("song", attr, value)
102
103 - def getObfuscatedScores(self):
104 s = {} 105 for difficulty in self.highScores.keys(): 106 s[difficulty.id] = [(score, stars, name, self.getScoreHash(difficulty, score, stars, name)) for score, stars, name in self.highScores[difficulty]] 107 return binascii.hexlify(Cerealizer.dumps(s))
108
109 - def save(self):
110 self._set("scores", self.getObfuscatedScores()) 111 112 f = open(self.fileName, "w") 113 self.info.write(f) 114 f.close()
115
116 - def _get(self, attr, type = None, default = ""):
117 try: 118 v = self.info.get("song", attr) 119 except: 120 v = default 121 if v is not None and type: 122 v = type(v) 123 return v
124
125 - def getDifficulties(self):
126 # Tutorials only have the medium difficulty 127 if self.tutorial: 128 return [difficulties[MEDIUM_DIFFICULTY]] 129 130 if self._difficulties is not None: 131 return self._difficulties 132 133 # See which difficulties are available 134 try: 135 noteFileName = os.path.join(os.path.dirname(self.fileName), "notes.mid") 136 info = MidiInfoReader() 137 midiIn = midi.MidiInFile(info, noteFileName) 138 try: 139 midiIn.read() 140 except MidiInfoReader.Done: 141 pass 142 info.difficulties.sort(lambda a, b: cmp(b.id, a.id)) 143 self._difficulties = info.difficulties 144 except: 145 self._difficulties = difficulties.values() 146 return self._difficulties
147
148 - def getName(self):
149 return self._get("name")
150
151 - def setName(self, value):
152 self._set("name", value)
153
154 - def getArtist(self):
155 return self._get("artist")
156
157 - def getCassetteColor(self):
158 c = self._get("cassettecolor") 159 if c: 160 return Theme.hexToColor(c)
161
162 - def setCassetteColor(self, color):
163 self._set("cassettecolor", Theme.colorToHex(color))
164
165 - def setArtist(self, value):
166 self._set("artist", value)
167
168 - def getScoreHash(self, difficulty, score, stars, name):
169 return sha.sha("%d%d%d%s" % (difficulty.id, score, stars, name)).hexdigest()
170
171 - def getDelay(self):
172 return self._get("delay", int, 0)
173
174 - def setDelay(self, value):
175 return self._set("delay", value)
176
177 - def getHighscores(self, difficulty):
178 try: 179 return self.highScores[difficulty] 180 except KeyError: 181 return []
182
183 - def uploadHighscores(self, url, songHash):
184 try: 185 d = { 186 "songName": self.songName, 187 "songHash": songHash, 188 "scores": self.getObfuscatedScores(), 189 "version": Version.version() 190 } 191 data = urllib.urlopen(url + "?" + urllib.urlencode(d)).read() 192 Log.debug("Score upload result: %s" % data) 193 if ";" in data: 194 fields = data.split(";") 195 else: 196 fields = [data, "0"] 197 return (fields[0] == "True", int(fields[1])) 198 except Exception, e: 199 Log.error(e) 200 return (False, 0)
201
202 - def addHighscore(self, difficulty, score, stars, name):
203 if not difficulty in self.highScores: 204 self.highScores[difficulty] = [] 205 self.highScores[difficulty].append((score, stars, name)) 206 self.highScores[difficulty].sort(lambda a, b: {True: -1, False: 1}[a[0] > b[0]]) 207 self.highScores[difficulty] = self.highScores[difficulty][:5] 208 for i, scores in enumerate(self.highScores[difficulty]): 209 _score, _stars, _name = scores 210 if _score == score and _stars == stars and _name == name: 211 return i 212 return -1
213
214 - def isTutorial(self):
215 return self._get("tutorial", int, 0) == 1
216 217 name = property(getName, setName) 218 artist = property(getArtist, setArtist) 219 delay = property(getDelay, setDelay) 220 tutorial = property(isTutorial) 221 difficulties = property(getDifficulties) 222 cassetteColor = property(getCassetteColor, setCassetteColor)
223
224 -class LibraryInfo(object):
225 - def __init__(self, libraryName, infoFileName):
226 self.libraryName = libraryName 227 self.fileName = infoFileName 228 self.info = ConfigParser() 229 self.songCount = 0 230 231 try: 232 self.info.read(infoFileName) 233 except: 234 pass 235 236 # Set a default name 237 if not self.name: 238 self.name = os.path.basename(os.path.dirname(self.fileName)) 239 240 # Count the available songs 241 libraryRoot = os.path.dirname(self.fileName) 242 for name in os.listdir(libraryRoot): 243 if not os.path.isdir(os.path.join(libraryRoot, name)) or name.startswith("."): 244 continue 245 if os.path.isfile(os.path.join(libraryRoot, name, "song.ini")): 246 self.songCount += 1
247
248 - def _set(self, attr, value):
249 if not self.info.has_section("library"): 250 self.info.add_section("library") 251 if type(value) == unicode: 252 value = value.encode(Config.encoding) 253 else: 254 value = str(value) 255 self.info.set("library", attr, value)
256
257 - def save(self):
258 f = open(self.fileName, "w") 259 self.info.write(f) 260 f.close()
261
262 - def _get(self, attr, type = None, default = ""):
263 try: 264 v = self.info.get("library", attr) 265 except: 266 v = default 267 if v is not None and type: 268 v = type(v) 269 return v
270
271 - def getName(self):
272 return self._get("name")
273
274 - def setName(self, value):
275 self._set("name", value)
276
277 - def getColor(self):
278 c = self._get("color") 279 if c: 280 return Theme.hexToColor(c)
281
282 - def setColor(self, color):
283 self._set("color", Theme.colorToHex(color))
284 285 286 name = property(getName, setName) 287 color = property(getColor, setColor)
288
289 -class Event:
290 - def __init__(self, length):
291 self.length = length
292
293 -class Note(Event):
294 - def __init__(self, number, length, special = False, tappable = False):
295 Event.__init__(self, length) 296 self.number = number 297 self.played = False 298 self.special = special 299 self.tappable = tappable
300
301 - def __repr__(self):
302 return "<#%d>" % self.number
303
304 -class Tempo(Event):
305 - def __init__(self, bpm):
306 Event.__init__(self, 0) 307 self.bpm = bpm
308
309 - def __repr__(self):
310 return "<%d bpm>" % self.bpm
311
312 -class TextEvent(Event):
313 - def __init__(self, text, length):
314 Event.__init__(self, length) 315 self.text = text
316
317 - def __repr__(self):
318 return "<%s>" % self.text
319
320 -class PictureEvent(Event):
321 - def __init__(self, fileName, length):
322 Event.__init__(self, length) 323 self.fileName = fileName
324
325 -class Track:
326 granularity = 50 327
328 - def __init__(self):
329 self.events = [] 330 self.allEvents = []
331
332 - def addEvent(self, time, event):
333 for t in range(int(time / self.granularity), int((time + event.length) / self.granularity) + 1): 334 if len(self.events) < t + 1: 335 n = t + 1 - len(self.events) 336 n *= 8 337 self.events = self.events + [[] for n in range(n)] 338 self.events[t].append((time - (t * self.granularity), event)) 339 self.allEvents.append((time, event))
340
341 - def removeEvent(self, time, event):
342 for t in range(int(time / self.granularity), int((time + event.length) / self.granularity) + 1): 343 e = (time - (t * self.granularity), event) 344 if t < len(self.events) and e in self.events[t]: 345 self.events[t].remove(e) 346 if (time, event) in self.allEvents: 347 self.allEvents.remove((time, event))
348
349 - def getEvents(self, startTime, endTime):
350 t1, t2 = [int(x) for x in [startTime / self.granularity, endTime / self.granularity]] 351 if t1 > t2: 352 t1, t2 = t2, t1 353 354 events = set() 355 for t in range(max(t1, 0), min(len(self.events), t2)): 356 for diff, event in self.events[t]: 357 time = (self.granularity * t) + diff 358 events.add((time, event)) 359 return events
360
361 - def getAllEvents(self):
362 return self.allEvents
363
364 - def reset(self):
365 for eventList in self.events: 366 for time, event in eventList: 367 if isinstance(event, Note): 368 event.played = False
369
370 - def update(self):
371 # Determine which notes are tappable. The rules are: 372 # 1. Not the first note of the track 373 # 2. Previous note not the same as this one 374 # 3. Previous note not a chord 375 # 4. Previous note ends at most 161 ticks before this one 376 bpm = None 377 ticksPerBeat = 480 378 tickThreshold = 161 379 prevNotes = [] 380 currentNotes = [] 381 currentTicks = 0.0 382 prevTicks = 0.0 383 epsilon = 1e-3 384 385 def beatsToTicks(time): 386 return (time * bpm * ticksPerBeat) / 60000.0
387 388 if not self.allEvents: 389 return 390 391 for time, event in self.allEvents + [self.allEvents[-1]]: 392 if isinstance(event, Tempo): 393 bpm = event.bpm 394 elif isinstance(event, Note): 395 # All notes are initially not tappable 396 event.tappable = False 397 ticks = beatsToTicks(time) 398 399 # Part of chord? 400 if ticks < currentTicks + epsilon: 401 currentNotes.append(event) 402 continue 403 404 """ 405 for i in range(5): 406 if i in [n.number for n in prevNotes]: 407 print " # ", 408 else: 409 print " . ", 410 print " | ", 411 for i in range(5): 412 if i in [n.number for n in currentNotes]: 413 print " # ", 414 else: 415 print " . ", 416 print 417 """ 418 419 # Previous note not a chord? 420 if len(prevNotes) == 1: 421 # Previous note ended recently enough? 422 prevEndTicks = prevTicks + beatsToTicks(prevNotes[0].length) 423 if currentTicks - prevEndTicks <= tickThreshold: 424 for note in currentNotes: 425 # Are any current notes the same as the previous one? 426 if note.number == prevNotes[0].number: 427 break 428 else: 429 # If all the notes are different, mark the current notes tappable 430 for note in currentNotes: 431 note.tappable = True 432 433 # Set the current notes as the previous notes 434 prevNotes = currentNotes 435 prevTicks = currentTicks 436 currentNotes = [event] 437 currentTicks = ticks
438
439 -class Song(object):
440 - def __init__(self, engine, infoFileName, songTrackName, guitarTrackName, rhythmTrackName, noteFileName, scriptFileName = None):
441 self.engine = engine 442 self.info = SongInfo(infoFileName) 443 self.tracks = [Track() for t in range(len(difficulties))] 444 self.difficulty = difficulties[AMAZING_DIFFICULTY] 445 self._playing = False 446 self.start = 0.0 447 self.noteFileName = noteFileName 448 self.bpm = None 449 self.period = 0 450 451 # load the tracks 452 if songTrackName: 453 self.music = Audio.Music(songTrackName) 454 455 self.guitarTrack = None 456 self.rhythmTrack = None 457 458 try: 459 if guitarTrackName: 460 self.guitarTrack = Audio.StreamingSound(self.engine, self.engine.audio.getChannel(1), guitarTrackName) 461 except Exception, e: 462 Log.warn("Unable to load guitar track: %s" % e) 463 464 try: 465 if rhythmTrackName: 466 self.rhythmTrack = Audio.StreamingSound(self.engine, self.engine.audio.getChannel(2), rhythmTrackName) 467 except Exception, e: 468 Log.warn("Unable to load rhythm track: %s" % e) 469 470 # load the notes 471 if noteFileName: 472 midiIn = midi.MidiInFile(MidiReader(self), noteFileName) 473 midiIn.read() 474 475 # load the script 476 if scriptFileName and os.path.isfile(scriptFileName): 477 scriptReader = ScriptReader(self, open(scriptFileName)) 478 scriptReader.read() 479 480 # update all note tracks 481 for track in self.tracks: 482 track.update()
483
484 - def getHash(self):
485 h = sha.new() 486 f = open(self.noteFileName, "rb") 487 bs = 1024 488 while True: 489 data = f.read(bs) 490 if not data: break 491 h.update(data) 492 return h.hexdigest()
493
494 - def setBpm(self, bpm):
495 self.bpm = bpm 496 self.period = 60000.0 / self.bpm
497
498 - def save(self):
499 self.info.save() 500 f = open(self.noteFileName + ".tmp", "wb") 501 midiOut = MidiWriter(self, midi.MidiOutFile(f)) 502 midiOut.write() 503 f.close() 504 505 # Rename the output file after it has been succesfully written 506 shutil.move(self.noteFileName + ".tmp", self.noteFileName)
507
508 - def play(self, start = 0.0):
509 self.start = start 510 self.music.play(0, start / 1000.0) 511 if self.guitarTrack: 512 assert start == 0.0 513 self.guitarTrack.play() 514 if self.rhythmTrack: 515 assert start == 0.0 516 self.rhythmTrack.play() 517 self._playing = True
518
519 - def pause(self):
520 self.music.pause() 521 self.engine.audio.pause()
522
523 - def unpause(self):
524 self.music.unpause() 525 self.engine.audio.unpause()
526
527 - def setGuitarVolume(self, volume):
528 if self.guitarTrack: 529 self.guitarTrack.setVolume(volume) 530 else: 531 self.music.setVolume(volume)
532
533 - def setRhythmVolume(self, volume):
534 if self.rhythmTrack: 535 self.rhythmTrack.setVolume(volume)
536
537 - def setBackgroundVolume(self, volume):
538 self.music.setVolume(volume)
539
540 - def stop(self):
541 for track in self.tracks: 542 track.reset() 543 544 self.music.stop() 545 self.music.rewind() 546 if self.guitarTrack: 547 self.guitarTrack.stop() 548 if self.rhythmTrack: 549 self.rhythmTrack.stop() 550 self._playing = False
551
552 - def fadeout(self, time):
553 for track in self.tracks: 554 track.reset() 555 556 self.music.fadeout(time) 557 if self.guitarTrack: 558 self.guitarTrack.fadeout(time) 559 if self.rhythmTrack: 560 self.rhythmTrack.fadeout(time) 561 self._playing = False
562
563 - def getPosition(self):
564 if not self._playing: 565 pos = 0.0 566 else: 567 pos = self.music.getPosition() 568 if pos < 0.0: 569 pos = 0.0 570 return pos + self.start
571
572 - def isPlaying(self):
573 return self._playing and self.music.isPlaying()
574
575 - def getBeat(self):
576 return self.getPosition() / self.period
577
578 - def update(self, ticks):
579 pass
580
581 - def getTrack(self):
582 return self.tracks[self.difficulty.id]
583 584 track = property(getTrack)
585 586 noteMap = { # difficulty, note 587 0x60: (AMAZING_DIFFICULTY, 0), 588 0x61: (AMAZING_DIFFICULTY, 1), 589 0x62: (AMAZING_DIFFICULTY, 2), 590 0x63: (AMAZING_DIFFICULTY, 3), 591 0x64: (AMAZING_DIFFICULTY, 4), 592 0x54: (MEDIUM_DIFFICULTY, 0), 593 0x55: (MEDIUM_DIFFICULTY, 1), 594 0x56: (MEDIUM_DIFFICULTY, 2), 595 0x57: (MEDIUM_DIFFICULTY, 3), 596 0x58: (MEDIUM_DIFFICULTY, 4), 597 0x48: (EASY_DIFFICULTY, 0), 598 0x49: (EASY_DIFFICULTY, 1), 599 0x4a: (EASY_DIFFICULTY, 2), 600 0x4b: (EASY_DIFFICULTY, 3), 601 0x4c: (EASY_DIFFICULTY, 4), 602 0x3c: (SUPAEASY_DIFFICULTY, 0), 603 0x3d: (SUPAEASY_DIFFICULTY, 1), 604 0x3e: (SUPAEASY_DIFFICULTY, 2), 605 0x3f: (SUPAEASY_DIFFICULTY, 3), 606 0x40: (SUPAEASY_DIFFICULTY, 4), 607 } 608 609 reverseNoteMap = dict([(v, k) for k, v in noteMap.items()]) 610
611 -class MidiWriter:
612 - def __init__(self, song, out):
613 self.song = song 614 self.out = out 615 self.ticksPerBeat = 480
616
617 - def midiTime(self, time):
618 return int(self.song.bpm * self.ticksPerBeat * time / 60000.0)
619
620 - def write(self):
621 self.out.header(division = self.ticksPerBeat) 622 self.out.start_of_track() 623 self.out.update_time(0) 624 if self.song.bpm: 625 self.out.tempo(int(60.0 * 10.0**6 / self.song.bpm)) 626 else: 627 self.out.tempo(int(60.0 * 10.0**6 / 122.0)) 628 629 # Collect all events 630 events = [zip([difficulty] * len(track.getAllEvents()), track.getAllEvents()) for difficulty, track in enumerate(self.song.tracks)] 631 events = reduce(lambda a, b: a + b, events) 632 events.sort(lambda a, b: {True: 1, False: -1}[a[1][0] > b[1][0]]) 633 heldNotes = [] 634 635 for difficulty, event in events: 636 time, event = event 637 if isinstance(event, Note): 638 time = self.midiTime(time) 639 640 # Turn of any held notes that were active before this point in time 641 for note, endTime in list(heldNotes): 642 if endTime <= time: 643 self.out.update_time(endTime, relative = 0) 644 self.out.note_off(0, note) 645 heldNotes.remove((note, endTime)) 646 647 note = reverseNoteMap[(difficulty, event.number)] 648 self.out.update_time(time, relative = 0) 649 self.out.note_on(0, note, event.special and 127 or 100) 650 heldNotes.append((note, time + self.midiTime(event.length))) 651 heldNotes.sort(lambda a, b: {True: 1, False: -1}[a[1] > b[1]]) 652 653 # Turn of any remaining notes 654 for note, endTime in heldNotes: 655 self.out.update_time(endTime, relative = 0) 656 self.out.note_off(0, note) 657 658 self.out.update_time(0) 659 self.out.end_of_track() 660 self.out.eof() 661 self.out.write()
662
663 -class ScriptReader:
664 - def __init__(self, song, scriptFile):
665 self.song = song 666 self.file = scriptFile
667
668 - def read(self):
669 for line in self.file.xreadlines(): 670 if line.startswith("#"): continue 671 time, length, type, data = re.split("[\t ]+", line.strip(), 3) 672 time = float(time) 673 length = float(length) 674 675 if type == "text": 676 event = TextEvent(data, length) 677 elif type == "pic": 678 event = PictureEvent(data, length) 679 else: 680 continue 681 682 for track in self.song.tracks: 683 track.addEvent(time, event)
684
685 -class MidiReader(midi.MidiOutStream):
686 - def __init__(self, song):
687 midi.MidiOutStream.__init__(self) 688 self.song = song 689 self.heldNotes = {} 690 self.velocity = {} 691 self.ticksPerBeat = 480 692 self.tempoMarkers = []
693
694 - def addEvent(self, track, event, time = None):
695 if time is None: 696 time = self.abs_time() 697 assert time >= 0 698 if track is None: 699 for t in self.song.tracks: 700 t.addEvent(time, event) 701 elif track < len(self.song.tracks): 702 self.song.tracks[track].addEvent(time, event)
703
704 - def abs_time(self):
705 def ticksToBeats(ticks, bpm): 706 return (60000.0 * ticks) / (bpm * self.ticksPerBeat)
707 708 if self.song.bpm: 709 currentTime = midi.MidiOutStream.abs_time(self) 710 711 # Find out the current scaled time. 712 # Yeah, this is reeally slow, but fast enough :) 713 scaledTime = 0.0 714 tempoMarkerTime = 0.0 715 currentBpm = self.song.bpm 716 for i, marker in enumerate(self.tempoMarkers): 717 time, bpm = marker 718 if time > currentTime: 719 break 720 scaledTime += ticksToBeats(time - tempoMarkerTime, currentBpm) 721 tempoMarkerTime, currentBpm = time, bpm 722 return scaledTime + ticksToBeats(currentTime - tempoMarkerTime, currentBpm) 723 return 0.0
724
725 - def header(self, format, nTracks, division):
726 self.ticksPerBeat = division
727
728 - def tempo(self, value):
729 bpm = 60.0 * 10.0**6 / value 730 self.tempoMarkers.append((midi.MidiOutStream.abs_time(self), bpm)) 731 if not self.song.bpm: 732 self.song.setBpm(bpm) 733 self.addEvent(None, Tempo(bpm))
734
735 - def note_on(self, channel, note, velocity):
736 if self.get_current_track() > 1: return 737 self.velocity[note] = velocity 738 self.heldNotes[(self.get_current_track(), channel, note)] = self.abs_time()
739
740 - def note_off(self, channel, note, velocity):
741 if self.get_current_track() > 1: return 742 try: 743 startTime = self.heldNotes[(self.get_current_track(), channel, note)] 744 endTime = self.abs_time() 745 del self.heldNotes[(self.get_current_track(), channel, note)] 746 if note in noteMap: 747 track, number = noteMap[note] 748 self.addEvent(track, Note(number, endTime - startTime, special = self.velocity[note] == 127), time = startTime) 749 else: 750 #Log.warn("MIDI note 0x%x at %d does not map to any game note." % (note, self.abs_time())) 751 pass 752 except KeyError: 753 Log.warn("MIDI note 0x%x on channel %d ending at %d was never started." % (note, channel, self.abs_time()))
754
755 -class MidiInfoReader(midi.MidiOutStream):
756 # We exit via this exception so that we don't need to read the whole file in
757 - class Done: pass
758
759 - def __init__(self):
760 midi.MidiOutStream.__init__(self) 761 self.difficulties = []
762
763 - def note_on(self, channel, note, velocity):
764 try: 765 track, number = noteMap[note] 766 diff = difficulties[track] 767 if not diff in self.difficulties: 768 self.difficulties.append(diff) 769 if len(self.difficulties) == len(difficulties): 770 raise Done 771 except KeyError: 772 pass
773
774 -def loadSong(engine, name, library = DEFAULT_LIBRARY, seekable = False, playbackOnly = False, notesOnly = False):
775 guitarFile = engine.resource.fileName(library, name, "guitar.ogg") 776 songFile = engine.resource.fileName(library, name, "song.ogg") 777 rhythmFile = engine.resource.fileName(library, name, "rhythm.ogg") 778 noteFile = engine.resource.fileName(library, name, "notes.mid", writable = True) 779 infoFile = engine.resource.fileName(library, name, "song.ini", writable = True) 780 scriptFile = engine.resource.fileName(library, name, "script.txt") 781 782 if seekable: 783 if os.path.isfile(guitarFile) and os.path.isfile(songFile): 784 # TODO: perform mixing here 785 songFile = guitarFile 786 guitarFile = None 787 else: 788 songFile = guitarFile 789 guitarFile = None 790 791 if not os.path.isfile(songFile): 792 songFile = guitarFile 793 guitarFile = None 794 795 if not os.path.isfile(rhythmFile): 796 rhythmFile = None 797 798 if playbackOnly: 799 noteFile = None 800 801 song = Song(engine, infoFile, songFile, guitarFile, rhythmFile, noteFile, scriptFile) 802 return song
803
804 -def loadSongInfo(engine, name, library = DEFAULT_LIBRARY):
805 infoFile = engine.resource.fileName(library, name, "song.ini", writable = True) 806 return SongInfo(infoFile)
807
808 -def createSong(engine, name, guitarTrackName, backgroundTrackName, rhythmTrackName = None, library = DEFAULT_LIBRARY):
809 path = os.path.abspath(engine.resource.fileName(library, name, writable = True)) 810 os.makedirs(path) 811 812 guitarFile = engine.resource.fileName(library, name, "guitar.ogg", writable = True) 813 songFile = engine.resource.fileName(library, name, "song.ogg", writable = True) 814 noteFile = engine.resource.fileName(library, name, "notes.mid", writable = True) 815 infoFile = engine.resource.fileName(library, name, "song.ini", writable = True) 816 817 shutil.copy(guitarTrackName, guitarFile) 818 819 if backgroundTrackName: 820 shutil.copy(backgroundTrackName, songFile) 821 else: 822 songFile = guitarFile 823 guitarFile = None 824 825 if rhythmTrackName: 826 rhythmFile = engine.resource.fileName(library, name, "rhythm.ogg", writable = True) 827 shutil.copy(rhythmTrackName, rhythmFile) 828 else: 829 rhythmFile = None 830 831 f = open(noteFile, "wb") 832 m = midi.MidiOutFile(f) 833 m.header() 834 m.start_of_track() 835 m.update_time(0) 836 m.end_of_track() 837 m.eof() 838 m.write() 839 f.close() 840 841 song = Song(engine, infoFile, songFile, guitarFile, rhythmFile, noteFile) 842 song.info.name = name 843 song.save() 844 845 return song
846
847 -def getDefaultLibrary(engine):
848 return LibraryInfo(DEFAULT_LIBRARY, engine.resource.fileName(DEFAULT_LIBRARY, "library.ini"))
849
850 -def getAvailableLibraries(engine, library = DEFAULT_LIBRARY):
851 # Search for libraries in both the read-write and read-only directories 852 songRoots = [engine.resource.fileName(library), 853 engine.resource.fileName(library, writable = True)] 854 libraries = [] 855 libraryRoots = [] 856 857 for songRoot in songRoots: 858 for libraryRoot in os.listdir(songRoot): 859 libraryRoot = os.path.join(songRoot, libraryRoot) 860 if not os.path.isdir(libraryRoot): 861 continue 862 for name in os.listdir(libraryRoot): 863 if os.path.isfile(os.path.join(libraryRoot, name, "song.ini")): 864 if not libraryRoot in libraryRoots: 865 libName = library + os.path.join(libraryRoot.replace(songRoot, "")) 866 libraries.append(LibraryInfo(libName, os.path.join(libraryRoot, "library.ini"))) 867 libraryRoots.append(libraryRoot) 868 return libraries
869
870 -def getAvailableSongs(engine, library = DEFAULT_LIBRARY, includeTutorials = False):
871 # Search for songs in both the read-write and read-only directories 872 songRoots = [engine.resource.fileName(library), engine.resource.fileName(library, writable = True)] 873 names = [] 874 for songRoot in songRoots: 875 for name in os.listdir(songRoot): 876 if not os.path.isfile(os.path.join(songRoot, name, "song.ini")) or name.startswith("."): 877 continue 878 if not name in names: 879 names.append(name) 880 881 songs = [SongInfo(engine.resource.fileName(library, name, "song.ini", writable = True)) for name in names] 882 if not includeTutorials: 883 songs = [song for song in songs if not song.tutorial] 884 songs.sort(lambda a, b: cmp(a.name, b.name)) 885 return songs
886