1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
48 self.id = id
49 self.text = text
50
53
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
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
77
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
108
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
147
149 return self._get("name")
150
152 self._set("name", value)
153
155 return self._get("artist")
156
161
164
166 self._set("artist", value)
167
169 return sha.sha("%d%d%d%s" % (difficulty.id, score, stars, name)).hexdigest()
170
172 return self._get("delay", int, 0)
173
175 return self._set("delay", value)
176
178 try:
179 return self.highScores[difficulty]
180 except KeyError:
181 return []
182
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
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
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
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
237 if not self.name:
238 self.name = os.path.basename(os.path.dirname(self.fileName))
239
240
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
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
272 return self._get("name")
273
275 self._set("name", value)
276
281
284
285
286 name = property(getName, setName)
287 color = property(getColor, setColor)
288
292
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
302 return "<#%d>" % self.number
303
308
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
324
326 granularity = 50
327
329 self.events = []
330 self.allEvents = []
331
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
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
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
362 return self.allEvents
363
365 for eventList in self.events:
366 for time, event in eventList:
367 if isinstance(event, Note):
368 event.played = False
369
371
372
373
374
375
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
396 event.tappable = False
397 ticks = beatsToTicks(time)
398
399
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
420 if len(prevNotes) == 1:
421
422 prevEndTicks = prevTicks + beatsToTicks(prevNotes[0].length)
423 if currentTicks - prevEndTicks <= tickThreshold:
424 for note in currentNotes:
425
426 if note.number == prevNotes[0].number:
427 break
428 else:
429
430 for note in currentNotes:
431 note.tappable = True
432
433
434 prevNotes = currentNotes
435 prevTicks = currentTicks
436 currentNotes = [event]
437 currentTicks = ticks
438
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
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
471 if noteFileName:
472 midiIn = midi.MidiInFile(MidiReader(self), noteFileName)
473 midiIn.read()
474
475
476 if scriptFileName and os.path.isfile(scriptFileName):
477 scriptReader = ScriptReader(self, open(scriptFileName))
478 scriptReader.read()
479
480
481 for track in self.tracks:
482 track.update()
483
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
495 self.bpm = bpm
496 self.period = 60000.0 / self.bpm
497
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
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
520 self.music.pause()
521 self.engine.audio.pause()
522
526
528 if self.guitarTrack:
529 self.guitarTrack.setVolume(volume)
530 else:
531 self.music.setVolume(volume)
532
534 if self.rhythmTrack:
535 self.rhythmTrack.setVolume(volume)
536
539
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
562
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
573 return self._playing and self.music.isPlaying()
574
577
580
583
584 track = property(getTrack)
585
586 noteMap = {
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
613 self.song = song
614 self.out = out
615 self.ticksPerBeat = 480
616
618 return int(self.song.bpm * self.ticksPerBeat * time / 60000.0)
619
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
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
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
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
665 self.song = song
666 self.file = scriptFile
667
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
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):
703
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
712
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
726 self.ticksPerBeat = division
727
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
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
756
758
762
763 - def note_on(self, channel, note, velocity):
773
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
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
807
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
849
851
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
871
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