##################################################################### # -*- coding: iso-8859-1 -*- # # # # Frets on Fire # # Copyright (C) 2006 Sami Kyöstilä # # 2008 myfingershurt # # # # This program 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 2 # # of the License, or (at your option) any later version. # # # # This program 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 this program; if not, write to the Free Software # # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # # MA 02110-1301, USA. # ##################################################################### import midi import Log import Audio import Config import os import re import shutil import math import Config #stump: when we completely drop 2.4 support, change this to just "import hashlib" try: import hashlib except ImportError: import sha class hashlib: sha1 = sha.sha import binascii import Cerealizer import urllib import Version import Theme import copy import string import cPickle #stump: Cerealizer and sqlite3 don't seem to like each other that much... from Language import _ #stump: turns out the sqlite3 module didn't appear until Python 2.5... try: import sqlite3 canCache = True except ImportError: try: import pysqlite2.dbapi2 as sqlite3 # close enough canCache = True except ImportError: canCache = False DEFAULT_BPM = 120.0 DEFAULT_LIBRARY = "songs" EXP_DIF = 0 HAR_DIF = 1 MED_DIF = 2 EAS_DIF = 3 #MFH - special / global text-event tracks for the separated text-event list variable TK_SCRIPT = 0 #script.txt events TK_SECTIONS = 1 #Section events TK_GUITAR_SOLOS = 2 #Guitar Solo start/stop events TK_LYRICS = 3 #RB MIDI Lyric events TK_UNUSED_TEXT = 4 #Unused / other text events #code for adding tracks, inside song.py: #self.song.eventTracks[TK_SCRIPT].addEvent(self.abs_time(), event) #MFH - add an event to the script.txt track #self.song.eventTracks[TK_SECTIONS].addEvent(self.abs_time(), event) #MFH - add an event to the sections track #self.song.eventTracks[TK_GUITAR_SOLOS].addEvent(self.abs_time(), event) #MFH - add an event to the guitar solos track #self.song.eventTracks[TK_LYRICS].addEvent(self.abs_time(), event) #MFH - add an event to the lyrics track #self.song.eventTracks[TK_UNUSED_TEXT].addEvent(self.abs_time(), event) #MFH - add an event to the unused text track #code for accessing track objects, outside song.py: #self.song.eventTracks[Song.TK_SCRIPT] #self.song.eventTracks[Song.TK_SECTIONS] #self.song.eventTracks[Song.TK_GUITAR_SOLOS] #self.song.eventTracks[Song.TK_LYRICS] #self.song.eventTracks[Song.TK_UNUSED_TEXT] GUITAR_TRACK = 0 RHYTHM_TRACK = 1 DRUM_TRACK = 2 #FOF_TYPE = 0 #GH1_TYPE = 1 #GH2_TYPE = 2 #MFH MIDI_TYPE_GH = 0 #GH type MIDIs have starpower phrases marked with a long G8 note on that instrument's track MIDI_TYPE_RB = 1 #RB type MIDIs have overdrive phrases marked with a long G#9 note on that instrument's track MIDI_TYPE_WT = 2 #WT type MIDIs have six notes and HOPOs marked on F# of the track #MFH EARLY_HIT_WINDOW_NONE = 1 #GH1/RB1/RB2 = NONE EARLY_HIT_WINDOW_HALF = 2 #GH2/GH3/GHA/GH80's = HALF EARLY_HIT_WINDOW_FULL = 3 #FoF = FULL GUITAR_PART = 0 RHYTHM_PART = 1 BASS_PART = 2 LEAD_PART = 3 DRUM_PART = 4 VOCAL_PART = 5 PART_SORT = [0,2,3,1,4,5] # these put Lead before Rhythm in menus. SORT_PART = [0,3,1,2,4,5] instrumentDiff = { 0 : (lambda a: a.diffGuitar), 1 : (lambda a: a.diffGuitar), 2 : (lambda a: a.diffBass), 3 : (lambda a: a.diffGuitar), 4 : (lambda a: a.diffDrums), 5 : (lambda a: a.diffVocals) } class Part: def __init__(self, id, text): self.id = id self.text = text #self.logClassInits = Config.get("game", "log_class_inits") #if self.logClassInits == 1: # Log.debug("Part class init (song.py)...") def __cmp__(self, other): if isinstance(other, Part): return cmp(PART_SORT[self.id], PART_SORT[other.id]) else: return cmp(self.id, other) #if it's not being compared with a part, we probably want its real ID. def __str__(self): return self.text def __repr__(self): return self.text parts = { GUITAR_PART: Part(GUITAR_PART, _("Guitar")), RHYTHM_PART: Part(RHYTHM_PART, _("Rhythm")), BASS_PART: Part(BASS_PART, _("Bass")), LEAD_PART: Part(LEAD_PART, _("Lead")), DRUM_PART: Part(DRUM_PART, _("Drums")), VOCAL_PART: Part(VOCAL_PART, _("Vocals")), } class Difficulty: def __init__(self, id, text): self.id = id self.text = text #self.logClassInits = Config.get("game", "log_class_inits") #if self.logClassInits == 1: # Log.debug("Difficulty class init (song.py)...") def __cmp__(self, other): if isinstance(other, Difficulty): return cmp(self.id, other.id) else: return cmp(self.id, other) def __str__(self): return self.text def __repr__(self): return self.text difficulties = { EAS_DIF: Difficulty(EAS_DIF, _("Easy")), MED_DIF: Difficulty(MED_DIF, _("Medium")), HAR_DIF: Difficulty(HAR_DIF, _("Hard")), EXP_DIF: Difficulty(EXP_DIF, _("Expert")), } defaultSections = ["Start","1/4","1/2","3/4"] # stump: manage cache files class CacheManager(object): SCHEMA_VERSION = 5 #stump: current cache format version number def __init__(self): self.caches = {} def getCache(self, infoFileName): '''Given the path of a song.ini, return a SQLite connection to the associated cache.''' cachePath = os.path.dirname(os.path.dirname(infoFileName)) if self.caches.has_key(cachePath): return self.caches[cachePath] # The cache must be opened or created. oldcwd = os.getcwd() try: os.chdir(cachePath) #stump: work around bug in SQLite unicode path name handling try: conn = sqlite3.Connection('.fofix-cache') except sqlite3.OperationalError: conn = None return conn finally: os.chdir(oldcwd) # Check that the cache is completely initialized. try: v = conn.execute("SELECT `value` FROM `config` WHERE `key` = 'version'").fetchone()[0] mustReinitialize = (int(v) != self.SCHEMA_VERSION) except: mustReinitialize = True if mustReinitialize: # Clean out the database, then make our tables. for tbl in conn.execute("SELECT `name` FROM `sqlite_master` WHERE `type` = 'table'").fetchall(): conn.execute('DROP TABLE `%s`' % tbl) conn.commit() conn.execute('VACUUM') #stump: if you need to change the database schema, do it here, then bump the version number, a small bit above here. conn.execute('CREATE TABLE `config` (`key` STRING UNIQUE, `value` STRING)') conn.execute('CREATE TABLE `songinfo` (`hash` STRING UNIQUE, `info` STRING)') conn.execute('INSERT INTO `config` (`key`, `value`) VALUES (?, ?)', ('version', self.SCHEMA_VERSION)) conn.commit() self.caches[cachePath] = conn return conn cacheManager = CacheManager() # singleton instance del CacheManager class SongInfo(object): def __init__(self, infoFileName, songLibrary = DEFAULT_LIBRARY, allowCacheUsage = False): self.songName = os.path.basename(os.path.dirname(infoFileName)) self.fileName = infoFileName self.libraryNam = songLibrary[songLibrary.find(DEFAULT_LIBRARY):] self.info = Config.MyConfigParser() self._partDifficulties = {} self._parts = None self._midiStyle = None if Config.get("performance", "cache_song_metadata"): self.allowCacheUsage = allowCacheUsage #stump else: self.allowCacheUsage = False self.locked = False #MFH - want to read valid sections from the MIDI in for practice mode selection here: self._sections = None self.name = _("NoName") try: self.info.read(infoFileName) except: pass self.logClassInits = Config.get("game", "log_class_inits") if self.logClassInits == 1: Log.debug("SongInfo class init (song.py): " + self.name) self.logSections = Config.get("game", "log_sections") self.logUneditedMidis = Config.get("log", "log_unedited_midis") self.useUneditedMidis = Config.get("debug", "use_unedited_midis") if self.useUneditedMidis == 1: #auto #if os.path.exists(os.path.join(os.path.dirname(self.fileName), "notes-unedited.mid")) if os.path.isfile(os.path.join(os.path.dirname(self.fileName), "notes-unedited.mid")): notefile = "notes-unedited.mid" if self.logUneditedMidis == 1: Log.debug("notes-unedited.mid found, using instead of notes.mid! - " + self.name) else: notefile = "notes.mid" if self.logUneditedMidis == 1: Log.debug("notes-unedited.mid not found, using notes.mid - " + self.name) else: notefile = "notes.mid" if self.logUneditedMidis == 1: Log.debug("notes-unedited.mid not found, using notes.mid - " + self.name) self.noteFileName = os.path.join(os.path.dirname(self.fileName), notefile) # stump: Check the cache for the presence of this song. if canCache and self.allowCacheUsage: self.stateHash = hashlib.sha1(open(self.noteFileName, 'rb').read() + open(self.fileName, 'rb').read()).hexdigest() cache = cacheManager.getCache(self.fileName) if cache != None: try: #MFH - it crashes here on previews! result = cache.execute('SELECT `info` FROM `songinfo` WHERE `hash` = ?', [self.stateHash]).fetchone() except Exception, e: Log.error(str(e)) result = None if result is not None: try: self.__dict__.update(cPickle.loads(str(result[0]))) return except: # The entry is there but could not be loaded. # Nuke it and let it be rebuilt further down. cache.execute('DELETE FROM `songinfo` WHERE `hash` = ?', [self.stateHash]) # Read highscores and verify their hashes. # There ain't no security like security throught obscurity :) self.highScores = {} scores = self._get("scores", str, "") scores_ext = self._get("scores_ext", str, "") if scores: scores = Cerealizer.loads(binascii.unhexlify(scores)) if scores_ext: scores_ext = Cerealizer.loads(binascii.unhexlify(scores_ext)) for difficulty in scores.keys(): try: difficulty = difficulties[difficulty] except KeyError: continue for i, base_scores in enumerate(scores[difficulty.id]): score, stars, name, hash = base_scores if scores_ext != "": #Someone may have mixed extended and non extended try: if len(scores_ext[difficulty.id][i]) < 9: hash_ext, stars2, notesHit, notesTotal, noteStreak, modVersion, oldInfo, oldInfo2 = scores_ext[difficulty.id][i] handicapValue = 0 longHandicap = "None" originalScore = 0 else: hash_ext, stars2, notesHit, notesTotal, noteStreak, modVersion, handicapValue, longHandicap, originalScore = scores_ext[difficulty.id][i] scoreExt = (notesHit, notesTotal, noteStreak, modVersion, handicapValue, longHandicap, originalScore) except: hash_ext = 0 scoreExt = (0, 0, 0 , "RF-mod", 0, "None", 0) if self.getScoreHash(difficulty, score, stars, name) == hash: if scores_ext != "" and hash == hash_ext: self.addHighscore(difficulty, score, stars, name, part = parts[GUITAR_PART], scoreExt = scoreExt) else: self.addHighscore(difficulty, score, stars, name, part = parts[GUITAR_PART]) else: Log.warn("Weak hack attempt detected. Better luck next time.") self.highScoresRhythm = {} scores = self._get("scores_rhythm", str, "") scores_ext = self._get("scores_rhythm_ext", str, "") if scores: scores = Cerealizer.loads(binascii.unhexlify(scores)) if scores_ext: scores_ext = Cerealizer.loads(binascii.unhexlify(scores_ext)) for difficulty in scores.keys(): try: difficulty = difficulties[difficulty] except KeyError: continue #for score, stars, name, hash in scores[difficulty.id]: # if self.getScoreHash(difficulty, score, stars, name) == hash: # self.addHighscore(difficulty, score, stars, name, part = parts[RHYTHM_PART]) # else: # Log.warn("Weak hack attempt detected. Better luck next time.") for i, base_scores in enumerate(scores[difficulty.id]): score, stars, name, hash = base_scores if scores_ext != "": #Someone may have mixed extended and non extended try: if len(scores_ext[difficulty.id][i]) < 9: hash_ext, stars2, notesHit, notesTotal, noteStreak, modVersion, oldInfo, oldInfo2 = scores_ext[difficulty.id][i] handicapValue = 0 longHandicap = "None" originalScore = 0 else: hash_ext, stars2, notesHit, notesTotal, noteStreak, modVersion, handicapValue, longHandicap, originalScore = scores_ext[difficulty.id][i] scoreExt = (notesHit, notesTotal, noteStreak, modVersion, handicapValue, longHandicap, originalScore) except: hash_ext = 0 scoreExt = (0, 0, 0 , "RF-mod", 0, "None", 0) if self.getScoreHash(difficulty, score, stars, name) == hash: if scores_ext != "" and hash == hash_ext: self.addHighscore(difficulty, score, stars, name, part = parts[RHYTHM_PART], scoreExt = scoreExt) else: self.addHighscore(difficulty, score, stars, name, part = parts[RHYTHM_PART]) else: Log.warn("Weak hack attempt detected. Better luck next time.") self.highScoresBass = {} scores = self._get("scores_bass", str, "") scores_ext = self._get("scores_bass_ext", str, "") if scores: scores = Cerealizer.loads(binascii.unhexlify(scores)) if scores_ext: scores_ext = Cerealizer.loads(binascii.unhexlify(scores_ext)) for difficulty in scores.keys(): try: difficulty = difficulties[difficulty] except KeyError: continue #for score, stars, name, hash in scores[difficulty.id]: # if self.getScoreHash(difficulty, score, stars, name) == hash: # self.addHighscore(difficulty, score, stars, name, part = parts[BASS_PART]) # else: # Log.warn("Weak hack attempt detected. Better luck next time.") for i, base_scores in enumerate(scores[difficulty.id]): score, stars, name, hash = base_scores if scores_ext != "": #Someone may have mixed extended and non extended try: if len(scores_ext[difficulty.id][i]) < 9: hash_ext, stars2, notesHit, notesTotal, noteStreak, modVersion, oldInfo, oldInfo2 = scores_ext[difficulty.id][i] handicapValue = 0 longHandicap = "None" originalScore = 0 else: hash_ext, stars2, notesHit, notesTotal, noteStreak, modVersion, handicapValue, longHandicap, originalScore = scores_ext[difficulty.id][i] scoreExt = (notesHit, notesTotal, noteStreak, modVersion, handicapValue, longHandicap, originalScore) except: hash_ext = 0 scoreExt = (0, 0, 0 , "RF-mod", 0, "None", 0) if self.getScoreHash(difficulty, score, stars, name) == hash: if scores_ext != "" and hash == hash_ext: self.addHighscore(difficulty, score, stars, name, part = parts[BASS_PART], scoreExt = scoreExt) else: self.addHighscore(difficulty, score, stars, name, part = parts[BASS_PART]) else: Log.warn("Weak hack attempt detected. Better luck next time.") self.highScoresLead = {} scores = self._get("scores_lead", str, "") scores_ext = self._get("scores_lead_ext", str, "") if scores: scores = Cerealizer.loads(binascii.unhexlify(scores)) if scores_ext: scores_ext = Cerealizer.loads(binascii.unhexlify(scores_ext)) for difficulty in scores.keys(): try: difficulty = difficulties[difficulty] except KeyError: continue #for score, stars, name, hash in scores[difficulty.id]: # if self.getScoreHash(difficulty, score, stars, name) == hash: # self.addHighscore(difficulty, score, stars, name, part = parts[LEAD_PART]) # else: # Log.warn("Weak hack attempt detected. Better luck next time.") for i, base_scores in enumerate(scores[difficulty.id]): score, stars, name, hash = base_scores if scores_ext != "": #Someone may have mixed extended and non extended try: if len(scores_ext[difficulty.id][i]) < 9: hash_ext, stars2, notesHit, notesTotal, noteStreak, modVersion, oldInfo, oldInfo2 = scores_ext[difficulty.id][i] handicapValue = 0 longHandicap = "None" originalScore = 0 else: hash_ext, stars2, notesHit, notesTotal, noteStreak, modVersion, handicapValue, longHandicap, originalScore = scores_ext[difficulty.id][i] scoreExt = (notesHit, notesTotal, noteStreak, modVersion, handicapValue, longHandicap, originalScore) except: hash_ext = 0 scoreExt = (0, 0, 0 , "RF-mod", 0, "None", 0) if self.getScoreHash(difficulty, score, stars, name) == hash: if scores_ext != "" and hash == hash_ext: self.addHighscore(difficulty, score, stars, name, part = parts[LEAD_PART], scoreExt = scoreExt) else: self.addHighscore(difficulty, score, stars, name, part = parts[LEAD_PART]) else: Log.warn("Weak hack attempt detected. Better luck next time.") #myfingershurt: drums :) self.highScoresDrum = {} scores = self._get("scores_drum", str, "") scores_ext = self._get("scores_drum_ext", str, "") if scores: scores = Cerealizer.loads(binascii.unhexlify(scores)) if scores_ext: scores_ext = Cerealizer.loads(binascii.unhexlify(scores_ext)) for difficulty in scores.keys(): try: difficulty = difficulties[difficulty] except KeyError: continue #for score, stars, name, hash in scores[difficulty.id]: # if self.getScoreHash(difficulty, score, stars, name) == hash: # self.addHighscore(difficulty, score, stars, name, part = parts[DRUM_PART]) # else: # Log.warn("Weak hack attempt detected. Better luck next time.") for i, base_scores in enumerate(scores[difficulty.id]): score, stars, name, hash = base_scores if scores_ext != "": #Someone may have mixed extended and non extended try: if len(scores_ext[difficulty.id][i]) < 9: hash_ext, stars2, notesHit, notesTotal, noteStreak, modVersion, oldInfo, oldInfo2 = scores_ext[difficulty.id][i] handicapValue = 0 longHandicap = "None" originalScore = 0 else: hash_ext, stars2, notesHit, notesTotal, noteStreak, modVersion, handicapValue, longHandicap, originalScore = scores_ext[difficulty.id][i] scoreExt = (notesHit, notesTotal, noteStreak, modVersion, handicapValue, longHandicap, originalScore) except: hash_ext = 0 scoreExt = (0, 0, 0 , "RF-mod", 0, "None", 0) if self.getScoreHash(difficulty, score, stars, name) == hash: if scores_ext != "" and hash == hash_ext: self.addHighscore(difficulty, score, stars, name, part = parts[DRUM_PART], scoreExt = scoreExt) else: self.addHighscore(difficulty, score, stars, name, part = parts[DRUM_PART]) else: Log.warn("Weak hack attempt detected. Better luck next time.") #akedrou - vocals! self.highScoresVocal = {} scores = self._get("scores_vocal", str, "") scores_ext = self._get("scores_vocal_ext", str, "") if scores: scores = Cerealizer.loads(binascii.unhexlify(scores)) if scores_ext: scores_ext = Cerealizer.loads(binascii.unhexlify(scores_ext)) for difficulty in scores.keys(): try: difficulty = difficulties[difficulty] except KeyError: continue #for score, stars, name, hash in scores[difficulty.id]: # if self.getScoreHash(difficulty, score, stars, name) == hash: # self.addHighscore(difficulty, score, stars, name, part = parts[DRUM_PART]) # else: # Log.warn("Weak hack attempt detected. Better luck next time.") for i, base_scores in enumerate(scores[difficulty.id]): score, stars, name, hash = base_scores if scores_ext != "": #Someone may have mixed extended and non extended try: if len(scores_ext[difficulty.id][i]) < 9: hash_ext, stars2, notesHit, notesTotal, noteStreak, modVersion, oldInfo, oldInfo2 = scores_ext[difficulty.id][i] handicapValue = 0 longHandicap = "None" originalScore = 0 else: hash_ext, stars2, notesHit, notesTotal, noteStreak, modVersion, handicapValue, longHandicap, originalScore = scores_ext[difficulty.id][i] scoreExt = (notesHit, notesTotal, noteStreak, modVersion, handicapValue, longHandicap, originalScore) except: hash_ext = 0 scoreExt = (0, 0, 0 , "RF-mod", 0, "None", 0) if self.getScoreHash(difficulty, score, stars, name) == hash: if scores_ext != "" and hash == hash_ext: self.addHighscore(difficulty, score, stars, name, part = parts[DRUM_PART], scoreExt = scoreExt) else: self.addHighscore(difficulty, score, stars, name, part = parts[DRUM_PART]) else: Log.warn("Weak hack attempt detected. Better luck next time.") if canCache and self.allowCacheUsage: #stump: preload this stuff into the cache self.getParts() self.getSections() self.writeCache() # stump: Write this song's info into the cache. def writeCache(self): if canCache and self.allowCacheUsage: self.stateHash = hashlib.sha1(open(self.noteFileName, 'rb').read() + open(self.fileName, 'rb').read()).hexdigest() cache = cacheManager.getCache(self.fileName) pkl = cPickle.dumps(self.__dict__) if cache != None: cache.execute('INSERT OR REPLACE INTO `songinfo` (`hash`, `info`) VALUES (?, ?)', [self.stateHash, pkl]) def _set(self, attr, value): if not self.info.has_section("song"): self.info.add_section("song") if type(value) == unicode: value = value.encode(Config.encoding) else: value = str(value) self.info.set("song", attr, value) def getObfuscatedScores(self, part = parts[GUITAR_PART]): s = {} if part == parts[GUITAR_PART]: highScores = self.highScores elif part == parts[RHYTHM_PART]: highScores = self.highScoresRhythm elif part == parts[BASS_PART]: highScores = self.highScoresBass elif part == parts[LEAD_PART]: highScores = self.highScoresLead elif part == parts[DRUM_PART]: highScores = self.highScoresDrum elif part == parts[VOCAL_PART]: highScores = self.highScoresVocal else: highScores = self.highScores for difficulty in highScores.keys(): if isinstance(difficulty, Difficulty): diff = diff.id else: diff = difficulty s[diff] = [(score, stars, name, self.getScoreHash(difficulty, score, stars, name)) for score, stars, name, scores_ext in highScores[difficulty]] return binascii.hexlify(Cerealizer.dumps(s)) def getObfuscatedScoresExt(self, part = parts[GUITAR_PART]): s = {} if part == parts[GUITAR_PART]: highScores = self.highScores elif part == parts[RHYTHM_PART]: highScores = self.highScoresRhythm elif part == parts[BASS_PART]: highScores = self.highScoresBass elif part == parts[LEAD_PART]: highScores = self.highScoresLead elif part == parts[DRUM_PART]: highScores = self.highScoresDrum elif part == parts[VOCAL_PART]: highScores = self.highScoresVocal else: highScores = self.highScores for difficulty in highScores.keys(): if isinstance(difficulty, Difficulty): diff = diff.id else: diff = difficulty s[diff] = [(self.getScoreHash(difficulty, score, stars, name), stars) + scores_ext for score, stars, name, scores_ext in highScores[difficulty]] return binascii.hexlify(Cerealizer.dumps(s)) def save(self): if self.highScores != {}: self._set("scores", self.getObfuscatedScores(part = parts[GUITAR_PART])) self._set("scores_ext", self.getObfuscatedScoresExt(part = parts[GUITAR_PART])) if self.highScoresRhythm != {}: self._set("scores_rhythm", self.getObfuscatedScores(part = parts[RHYTHM_PART])) self._set("scores_rhythm_ext", self.getObfuscatedScoresExt(part = parts[RHYTHM_PART])) if self.highScoresBass != {}: self._set("scores_bass", self.getObfuscatedScores(part = parts[BASS_PART])) self._set("scores_bass_ext", self.getObfuscatedScoresExt(part = parts[BASS_PART])) if self.highScoresLead != {}: self._set("scores_lead", self.getObfuscatedScores(part = parts[LEAD_PART])) self._set("scores_lead_ext", self.getObfuscatedScoresExt(part = parts[LEAD_PART])) #myfingershurt: drums :) if self.highScoresDrum != {}: self._set("scores_drum", self.getObfuscatedScores(part = parts[DRUM_PART])) self._set("scores_drum_ext", self.getObfuscatedScoresExt(part = parts[DRUM_PART])) #akedrou if self.highScoresVocal != {}: self._set("scores_vocal", self.getObfuscatedScores(part = parts[VOCAL_PART])) self._set("scores_vocal_ext", self.getObfuscatedScoresExt(part = parts[VOCAL_PART])) if os.access(os.path.dirname(self.fileName), os.W_OK) == True: f = open(self.fileName, "w") self.info.write(f) f.close() def _get(self, attr, type = None, default = ""): try: v = self.info.get("song", attr) except: v = default if v == "": #key found, but empty - need to catch as int("") will burn. v = default if v is not None and type: v = type(v) return v # def getDifficulties(self): # # Tutorials only have the medium difficulty # if self.tutorial: # return [difficulties[HAR_DIF]] # # if self._difficulties is not None: # return self._difficulties # # # See which difficulties are available # try: # # noteFileName = self.noteFileName # # Log.debug("Retrieving difficulties from: " + noteFileName) # info = MidiInfoReaderNoSections() # midiIn = midi.MidiInFile(info, noteFileName) # try: # midiIn.read() # except MidiInfoReaderNoSections.Done: # pass # info.difficulties.sort(lambda a, b: cmp(b.id, a.id)) # self._difficulties = info.difficulties # except: # self._difficulties = difficulties.values() # return self._difficulties def getPartDifficulties(self): if len(self._partDifficulties) is not 0: return self._partDifficulties self.getParts() return self._partDifficulties partDifficulties = property(getPartDifficulties) def getMidiStyle(self): if self._midiStyle is not None: return self._midiStyle self.getParts() return self._midiStyle midiStyle = property(getMidiStyle) def getParts(self): if self._parts is not None: return self._parts # See which parts are available try: noteFileName = self.noteFileName Log.debug("Retrieving parts from: " + noteFileName) info = MidiPartsDiffReader() midiIn = midi.MidiInFile(info, noteFileName) midiIn.read() if info.parts == []: Log.debug("Improperly named tracks. Attempting to force first track guitar.") info = MidiPartsDiffReader(forceGuitar = True) midiIn = midi.MidiInFile(info, noteFileName) midiIn.read() if info.parts == []: Log.warn("No tracks found!") raise Exception self._midiStyle = info.midiStyle info.parts.sort(lambda b, a: cmp(b.id, a.id)) self._parts = info.parts for part in info.parts: if self.tutorial: self._partDifficulties[part.id] = [difficulties[HAR_DIF]] continue info.difficulties[part.id].sort(lambda a, b: cmp(a.id, b.id)) self._partDifficulties[part.id] = info.difficulties[part.id] except: Log.warn("Note file not parsed correctly. Selected part and/or difficulty may not be available.") self._parts = parts.values() for part in self._parts: self._partDifficulties[part.id] = difficulties.values() return self._parts def getName(self): return self._get("name") def setName(self, value): self._set("name", value) def getArtist(self): return self._get("artist") def getAlbum(self): return self._get("album") def getGenre(self): return self._get("genre") def getIcon(self): return self._get("icon") def getBossBattle(self): return self._get("boss_battle") def getDiffSong(self): return self._get("diff_band", int, -1) def getDiffGuitar(self): return self._get("diff_guitar", int, -1) def getDiffDrums(self): return self._get("diff_drums", int, -1) def getDiffBass(self): return self._get("diff_bass", int, -1) def getDiffVocals(self): return self._get("diff_vocals", int, -1) def getYear(self): return self._get("year") def getLoading(self): return self._get("loading_phrase") def getCassetteColor(self): c = self._get("cassettecolor") if c: return Theme.hexToColor(c) def setCassetteColor(self, color): self._set("cassettecolor", Theme.colorToHex(color)) def setArtist(self, value): self._set("artist", value) def setAlbum(self, value): self._set("album", value) def setGenre(self, value): self._set("genre", value) def setIcon(self, value): self._set("icon", value) def setBossBattle(self, value): self._set("boss_battle", value) def setDiffSong(self, value): self._set("diff_band", value) def setDiffGuitar(self, value): self._set("diff_guitar", value) def setDiffDrums(self, value): self._set("diff_drums", value) def setDiffBass(self, value): self._set("diff_bass", value) def setDiffVocals(self, value): self._set("diff_vocals", value) def setYear(self, value): self._set("year", value) def setLoading(self, value): self._set("loading_phrase", value) def getScoreHash(self, difficulty, score, stars, name): if isinstance(difficulty, Difficulty): difficulty = difficulty.id return hashlib.sha1("%d%d%d%s" % (difficulty, score, stars, name)).hexdigest() def getDelay(self): return self._get("delay", int, 0) def setDelay(self, value): return self._set("delay", value) def getFrets(self): return self._get("frets") def setFrets(self, value): self._set("frets", value) def getVersion(self): return self._get("version") def setVersion(self, value): self._set("version", value) def getTags(self): return self._get("tags") def setTags(self, value): self._set("tags", value) def getHopo(self): return self._get("hopo") def setHopo(self, value): self._set("hopo", value) def getCount(self): return self._get("count") def setCount(self, value): self._set("count", value) def getEighthNoteHopo(self): return self._get("eighthnote_hopo") def setEighthNoteHopo(self, value): self._set("eighthnote_hopo", value) def getLyrics(self): return self._get("lyrics") def setLyrics(self, value): self._set("lyrics", value) def getHighscoresWithPartString(self, difficulty, part = str(parts[GUITAR_PART]) ): if part == str(parts[GUITAR_PART]): highScores = self.highScores elif part == str(parts[RHYTHM_PART]): highScores = self.highScoresRhythm elif part == str(parts[BASS_PART]): highScores = self.highScoresBass elif part == str(parts[LEAD_PART]): highScores = self.highScoresLead #myfingershurt: drums :) elif part == str(parts[DRUM_PART]): highScores = self.highScoresDrum elif part == str(parts[VOCAL_PART]): #akedrou - vocals! highScores = self.highScoresVocal else: highScores = self.highScores try: return highScores[difficulty.id] except KeyError: return [] def getHighscores(self, difficulty, part = parts[GUITAR_PART]): if part == parts[GUITAR_PART]: highScores = self.highScores elif part == parts[RHYTHM_PART]: highScores = self.highScoresRhythm elif part == parts[BASS_PART]: highScores = self.highScoresBass elif part == parts[LEAD_PART]: highScores = self.highScoresLead #myfingershurt: drums :) elif part == parts[DRUM_PART]: highScores = self.highScoresDrum elif part == parts[VOCAL_PART]: #akedrou - vocals! highScores = self.highScoresVocal else: highScores = self.highScores try: return highScores[difficulty.id] except KeyError: return [] def uploadHighscores(self, url, songHash, part = parts[GUITAR_PART]): if part == parts[VOCAL_PART]: #not implemented return False try: d = { "songName": self.songName, "songHash": songHash, "scores": self.getObfuscatedScores(part = part), "scores_ext": self.getObfuscatedScoresExt(part = part), "version": "%s-3.100" % Version.appNameSexy(), "songPart": part } data = urllib.urlopen(url + "?" + urllib.urlencode(d)).read() Log.debug("Score upload result: %s" % data) #return data == "True" return data #MFH - want to return the actual result data. except Exception, e: Log.error("Score upload error: %s" % e) return False return True def addHighscore(self, difficulty, score, stars, name, part = parts[GUITAR_PART], scoreExt = (0, 0, 0, "RF-mod", 0, "None", 0)): if part == parts[GUITAR_PART]: highScores = self.highScores elif part == parts[RHYTHM_PART]: highScores = self.highScoresRhythm elif part == parts[BASS_PART]: highScores = self.highScoresBass elif part == parts[LEAD_PART]: highScores = self.highScoresLead #myfingershurt: drums :) elif part == parts[DRUM_PART]: highScores = self.highScoresDrum elif part == parts[VOCAL_PART]: #akedrou - vocals! highScores = self.highScoresVocal else: highScores = self.highScores if not difficulty.id in highScores: highScores[difficulty.id] = [] highScores[difficulty.id].append((score, stars, name, scoreExt)) highScores[difficulty.id].sort(lambda a, b: {True: -1, False: 1}[a[0] > b[0]]) highScores[difficulty.id] = highScores[difficulty.id][:5] for i, scores in enumerate(highScores[difficulty.id]): _score, _stars, _name, _scores_ext = scores if _score == score and _stars == stars and _name == name: return i return -1 def isTutorial(self): return self._get("tutorial", int, 0) == 1 def findTag(self, find, value = None): for tag in self.tags.split(','): temp = tag.split('=') if find == temp[0]: if value == None: return True elif len(temp) == 2 and value == temp[1]: return True return False def getSections(self): #MFH if self._sections is not None: return self._sections # See which sections are available try: noteFileName = self.noteFileName Log.debug("Retrieving sections from: " + noteFileName) info = MidiSectionReader() midiIn = midi.MidiInFile(info, noteFileName) try: midiIn.read() except MidiSectionReader.Done: pass self._sections = info.sections if len(self._sections) <= 1: self._sections = info.noteCountSections self._sections.insert(0,["0:00 -> Start", 0.0]) #MFH - only log if enabled Log.warn("Song.py: Using auto-generated note count sections...") if self.logSections == 1: Log.debug("Practice sections: " + str(self._sections)) else: self._sections.insert(0,["0:00 -> Start", 0.0]) #MFH - only log if enabled if self.logSections == 1: Log.debug("Practice sections: " + str(self._sections)) except Exception, e: Log.warn("Song.py: Unable to retrieve section names for practice mode selection: %s" % e) self._sections = None return self._sections #coolguy567's unlock system def getUnlockID(self): return self._get("unlock_id") def getUnlockRequire(self): return self._get("unlock_require") def getUnlockText(self): return self._get("unlock_text", default = _("This song is locked.")) # This takes True or False, not the value in the ini def setCompleted(self, value): if value: self._set("unlock_completed", len(removeSongOrderPrefixFromName(self.name)) * len(self.artist) + 1) else: self._set("unlock_completed", 0) # This returns True or False, not the value in the ini def getCompleted(self): iniValue = self._get("unlock_completed", int, default = 0) if iniValue == len(removeSongOrderPrefixFromName(self.name)) * len(self.artist) + 1: # yay, security through obscurity! return True else: return False def setLocked(self, value): self.locked = value # WARNING this will only work on a SongInfo loaded via getAvailableSongs # (yeah, I know) def getLocked(self): return self.locked #MFH - adding song.ini setting to allow fretter to specify early hit window size (none, half, or full) def getEarlyHitWindowSize(self): #MFH return self._get("early_hit_window_size", str) def setEarlyHitWindowSize(self, value): #MFH self._set("early_hit_window_size", value) def getHopoFreq(self): #MFH return self._get("hopofreq") def setHopoFreq(self, value): #MFH self._set("hopofreq", value) name = property(getName, setName) artist = property(getArtist, setArtist) year = property(getYear, setYear) loading = property(getLoading, setLoading) delay = property(getDelay, setDelay) tutorial = property(isTutorial) # difficulties = property(getDifficulties) cassetteColor = property(getCassetteColor, setCassetteColor) #New RF-mod Items parts = property(getParts) frets = property(getFrets, setFrets) version = property(getVersion, setVersion) tags = property(getTags, setTags) hopo = property(getHopo, setHopo) count = property(getCount, setCount) lyrics = property(getLyrics, setLyrics) EighthNoteHopo = property(getEighthNoteHopo, setEighthNoteHopo) hopofreq = property(getHopoFreq, setHopoFreq) #MFH #New rb2 setlist album = property(getAlbum, setAlbum) genre = property(getGenre, setGenre) icon = property(getIcon, setIcon) diffSong = property(getDiffSong, setDiffSong) diffGuitar = property(getDiffGuitar, setDiffGuitar) diffBass = property(getDiffBass, setDiffBass) diffDrums = property(getDiffDrums, setDiffDrums) diffVocals = property(getDiffVocals, setDiffVocals) #boss battles bossBattle = property(getBossBattle, setBossBattle) #May no longer be necessary folder = False completed = property(getCompleted, setCompleted) sections = property(getSections) #MFH early_hit_window_size = property(getEarlyHitWindowSize, setEarlyHitWindowSize) #MFH class LibraryInfo(object): def __init__(self, libraryName, infoFileName): self.libraryName = libraryName self.fileName = infoFileName self.info = Config.MyConfigParser() self.songCount = 0 self.artist = None self.logClassInits = Config.get("game", "log_class_inits") if self.logClassInits == 1: Log.debug("LibraryInfo class init (song.py)...") try: self.info.read(infoFileName) except: pass # Set a default name if not self.name: self.name = os.path.basename(os.path.dirname(self.fileName)) if Config.get("performance", "disable_libcount") == True: return # Count the available songs libraryRoot = os.path.dirname(self.fileName) for name in os.listdir(libraryRoot): if not os.path.isdir(os.path.join(libraryRoot, name)) or name.startswith("."): #if not os.path.isdir(os.path.join(libraryRoot, name)): continue if os.path.isfile(os.path.join(libraryRoot, name, "notes.mid")): self.songCount += 1 elif os.path.isfile(os.path.join(libraryRoot, name, "notes-unedited.mid")): self.songCount += 1 def _set(self, attr, value): if not self.info.has_section("library"): self.info.add_section("library") if type(value) == unicode: value = value.encode(Config.encoding) else: value = str(value) self.info.set("library", attr, value) def save(self): if os.access(os.path.dirname(self.fileName), os.W_OK) == True: f = open(self.fileName, "w") self.info.write(f) f.close() def _get(self, attr, type = None, default = ""): try: v = self.info.get("library", attr) except: v = default if v is not None and type: v = type(v) return v def getName(self): return self._get("name") def setName(self, value): self._set("name", value) def getColor(self): c = self._get("color") if c: return Theme.hexToColor(c) def setColor(self, color): self._set("color", Theme.colorToHex(color)) name = property(getName, setName) color = property(getColor, setColor) class BlankSpaceInfo(object): #MFH #def __init__(self, config, section): def __init__(self, nameToDisplay = ""): #self.info = config #self.section = section self.nameToDisplay = nameToDisplay self.logClassInits = Config.get("game", "log_class_inits") if self.logClassInits == 1: Log.debug("BlankSpaceInfo class init (song.py)...") self.artist = None #MFH - prevents search errors def _set(self, attr, value): if type(value) == unicode: value = value.encode(Config.encoding) else: value = str(value) self.info.set(self.section, attr, value) def _get(self, attr, type = None, default = ""): try: v = self.info.get(self.section, attr) except: v = default if v is not None and type: v = type(v) return v def getName(self): #return self._get("name") return self.nameToDisplay def setName(self, value): self._set("name", value) def getColor(self): c = self._get("color") if c: return Theme.hexToColor(c) def setColor(self, color): self._set("color", Theme.colorToHex(color)) def getUnlockID(self): return "" name = property(getName, setName) color = property(getColor, setColor) class CareerResetterInfo(object): #MFH #def __init__(self, config, section): def __init__(self): #self.info = config #self.section = section self.logClassInits = Config.get("game", "log_class_inits") if self.logClassInits == 1: Log.debug("CareerResetterInfo class init (song.py)...") self.artist = None #MFH - prevents search errors def _set(self, attr, value): if type(value) == unicode: value = value.encode(Config.encoding) else: value = str(value) self.info.set(self.section, attr, value) def _get(self, attr, type = None, default = ""): try: v = self.info.get(self.section, attr) except: v = default if v is not None and type: v = type(v) return v def getName(self): #return self._get("name") return _("Reset Career") def setName(self, value): self._set("name", value) def getColor(self): c = self._get("color") if c: return Theme.hexToColor(c) def setColor(self, color): self._set("color", Theme.colorToHex(color)) name = property(getName, setName) color = property(getColor, setColor) class RandomSongInfo(object): #MFH #def __init__(self, config, section): def __init__(self): #self.info = config #self.section = section self.logClassInits = Config.get("game", "log_class_inits") if self.logClassInits == 1: Log.debug("RandomSongInfo class init (song.py)...") self.artist = None #MFH - prevents search errors def _set(self, attr, value): if type(value) == unicode: value = value.encode(Config.encoding) else: value = str(value) self.info.set(self.section, attr, value) def _get(self, attr, type = None, default = ""): try: v = self.info.get(self.section, attr) except: v = default if v is not None and type: v = type(v) return v def getName(self): #return self._get("name") return _(" (Random Song)") def setName(self, value): self._set("name", value) def getColor(self): c = self._get("color") if c: return Theme.hexToColor(c) def setColor(self, color): self._set("color", Theme.colorToHex(color)) name = property(getName, setName) color = property(getColor, setColor) #coolguy567's unlock system class TitleInfo(object): def __init__(self, config, section): self.info = config self.section = section self.logClassInits = Config.get("game", "log_class_inits") if self.logClassInits == 1: Log.debug("TitleInfo class init (song.py)...") self.artist = None #MFH - prevents search errors def _set(self, attr, value): if type(value) == unicode: value = value.encode(Config.encoding) else: value = str(value) self.info.set(self.section, attr, value) def _get(self, attr, type = None, default = ""): try: v = self.info.get(self.section, attr) except: v = default if v is not None and type: v = type(v) return v def getName(self): return self._get("name") def setName(self, value): self._set("name", value) def getColor(self): c = self._get("color") if c: return Theme.hexToColor(c) def setColor(self, color): self._set("color", Theme.colorToHex(color)) def getUnlockID(self): return self._get("unlock_id") name = property(getName, setName) color = property(getColor, setColor) class SortTitleInfo(object): def __init__(self, nameToDisplay): self.nameToDisplay = nameToDisplay self.logClassInits = Config.get("game", "log_class_inits") if self.logClassInits == 1: Log.debug("TitleInfo class init (song.py)...") self.artist = None #MFH - prevents search errors def _set(self, attr, value): if type(value) == unicode: value = value.encode(Config.encoding) else: value = str(value) self.info.set(self.section, attr, value) def _get(self, attr, type = None, default = ""): try: v = self.info.get(self.section, attr) except: v = default if v is not None and type: v = type(v) return v def getName(self): return self.nameToDisplay def setName(self, value): self._set("name", value) def getColor(self): c = self._get("color") if c: return Theme.hexToColor(c) def setColor(self, color): self._set("color", Theme.colorToHex(color)) def getUnlockID(self): return self.nameToDisplay name = property(getName, setName) color = property(getColor, setColor) class Event: def __init__(self, length): self.length = length class MarkerNote(Event): #MFH def __init__(self, number, length, endMarker = False): Event.__init__(self, length) self.number = number self.endMarker = endMarker self.happened = False def __repr__(self): return "<#%d>" % self.number class Note(Event): def __init__(self, number, length, special = False, tappable = 0, star = False, finalStar = False): Event.__init__(self, length) self.number = number self.played = False self.special = special self.tappable = tappable #RF-mod self.hopod = False self.skipped = False self.flameCount = 0 self.star = star self.finalStar = finalStar self.noteBpm = 0.0 def __repr__(self): return "<#%d>" % self.number class VocalNote(Event): def __init__(self, note, length, tap = False): Event.__init__(self, length) self.note = note self.phrase = 0 self.played = False self.stopped = False self.accuracy = 0.0 self.tap = tap self.speak = False self.extra = False #not sure what this is yet - "^" self.lyric = None self.heldNote = False class Bars(Event): def __init__(self, barType): Event.__init__(self, barType) self.barType = barType #0 half-beat, 1 beat, 2 measure self.soundPlayed = False def __repr__(self): return "<#%d>" % self.barType class Tempo(Event): def __init__(self, bpm): Event.__init__(self, 0) self.bpm = bpm def __repr__(self): return "<%d bpm>" % self.bpm class TextEvent(Event): def __init__(self, text, length): Event.__init__(self, length) self.text = text def __repr__(self): return "<%s>" % self.text class PictureEvent(Event): def __init__(self, fileName, length): Event.__init__(self, length) self.fileName = fileName class Track: #MFH - Changing Track class to a base class. NoteTrack and TempoTrack will extend it. granularity = 50 def __init__(self, engine): self.events = [] self.allEvents = [] self.marked = False self.currentIndex = None #MFH self.maxIndex = None #MFH if engine is not None: self.logClassInits = Config.get("game", "log_class_inits") if self.logClassInits == 1: Log.debug("Track class init (song.py)...") #self.chordFudge = 1 #self.hopoTick = engine.config.get("coffee", "hopo_frequency") #self.hopoTickCheat = engine.config.get("coffee", "hopo_freq_cheat") #self.songHopoFreq = engine.config.get("game", "song_hopo_freq") def __getitem__(self, index): #if isinstance(index, slice): # return [x**2 for x in index] #else: # return index**2 #return index**2 return self.allEvents[index] def __len__(self): return len(self.allEvents) def addEvent(self, time, event): for t in range(int(time / self.granularity), int((time + event.length) / self.granularity) + 1): if len(self.events) < t + 1: n = t + 1 - len(self.events) n *= 8 self.events = self.events + [[] for n in range(n)] self.events[t].append((time - (t * self.granularity), event)) self.allEvents.append((time, event)) if self.maxIndex == None: #MFH - tracking track size self.maxIndex = 0 self.currentIndex = 0 else: self.maxIndex += 1 def removeEvent(self, time, event): for t in range(int(time / self.granularity), int((time + event.length) / self.granularity) + 1): e = (time - (t * self.granularity), event) if t < len(self.events) and e in self.events[t]: self.events[t].remove(e) if (time, event) in self.allEvents: self.allEvents.remove((time, event)) #MFH - tracking track size if self.maxIndex != None: self.maxIndex -= 1 if self.maxIndex < 0: self.maxIndex = None self.currentIndex = None def getNextEvent(self, lookAhead = 0): #MFH if self.maxIndex != None and self.currentIndex != None: #lookAhead > 0 means look that many indices ahead of the Next event if (self.currentIndex + lookAhead) <= self.maxIndex: return self.allEvents[self.currentIndex + lookAhead] return None def getPrevEvent(self, lookBehind = 0): #MFH - lookBehind of 0 = return previous event. if self.maxIndex != None and self.currentIndex != None: #lookBehind > 0 means look that many indices behind of the Prev event if (self.currentIndex - 1 - lookBehind) >= 0: return self.allEvents[self.currentIndex - 1 - lookBehind] return None def getEvents(self, startTime, endTime): t1, t2 = [int(x) for x in [startTime / self.granularity, endTime / self.granularity]] if t1 > t2: t1, t2 = t2, t1 events = [] for t in range(max(t1, 0), min(len(self.events), t2)): for diff, event in self.events[t]: time = (self.granularity * t) + diff if not (time, event) in events: events.append((time, event)) return events def getAllEvents(self): return self.allEvents def reset(self): if self.maxIndex: self.currentIndex = 0 for eventList in self.events: for time, event in eventList: if isinstance(event, Note): event.played = False event.hopod = False event.skipped = False event.flameCount = 0 if isinstance(event, MarkerNote): event.happened = False class VocalTrack(Track): def __init__(self, engine): self.allNotes = {} self.allWords = {} self.starTimes = [] self.minPitch = 127 self.maxPitch = 0 self.logTempoEvents = 0 if engine: self.logTempoEvents = engine.config.get("log", "log_tempo_events") Track.__init__(self, engine) def getAllNotes(self): return self.allNotes def addEvent(self, time, event): if isinstance(event, VocalNote) or isinstance(event, VocalPhrase): Track.addEvent(self, time, event) def removeTempoEvents(self): for time, event in self.allEvents: if isinstance(event, Tempo): self.allEvents.remove((time, event)) if self.logTempoEvents == 1: Log.debug("Tempo event removed from VocalTrack during cleanup: " + str(event.bpm) + "bpm") def markPhrases(self): phraseId = 0 stars = [] phraseTimes = [] markStars = False if len(self.starTimes) < 2: markStars = True Log.warn("This song does not appear to have any vocal star power events - falling back on auto-generation.") for time, event in self.getAllEvents(): if isinstance(event, VocalPhrase): if time in self.starTimes and not markStars: event.star = True if time in phraseTimes: Log.warn("Phrase repeated - some lyric phrase errors may occur.") #self.removeEvent(time, event) phraseId += 1 continue if markStars and phraseId+1 % 7 == 0: event.star = True phraseTimes.append(time) phraseId += 1 index = 0 for time, tuple in self.allNotes.iteritems(): phraseId = 0 for i, phraseTime in enumerate(self.getAllEvents()): if time > phraseTime[0] and time < phraseTime[0] + phraseTime[1].length: phraseId = i if phraseId < 0: phraseId = 0 break tuple[1].phrase = phraseId if not tuple[1].tap: try: lyric = self.allWords[tuple[0]] except KeyError: lyric = None if lyric: if lyric.find("+") >= 0: tuple[1].heldNote = True else: if lyric.find("#") >= 0: tuple[1].speak = True tuple[1].lyric = lyric.strip("#") elif lyric.find("^") >= 0: tuple[1].extra = True tuple[1].lyric = lyric.strip("^") else: tuple[1].lyric = lyric else: self.allEvents[phraseId][1].tapPhrase = True self.allEvents[phraseId][1].addEvent(tuple[0], tuple[1]) self.allEvents[phraseId][1].minPitch = min(self.allEvents[phraseId][1].minPitch, tuple[1].note) self.allEvents[phraseId][1].maxPitch = max(self.allEvents[phraseId][1].maxPitch, tuple[1].note) for time, event in self.getAllEvents(): if isinstance(event, VocalPhrase): event.sort() def reset(self): if self.maxIndex: self.currentIndex = 0 for eventList in self.events: for time, event in eventList: if isinstance(event, VocalPhrase): for time, note in event.allEvents: note.played = False note.stopped = False note.accuracy = 0.0 class VocalPhrase(VocalTrack, Event): def __init__(self, length, star = False): Event.__init__(self, length) VocalTrack.__init__(self, engine = None) self.star = star self.tapPhrase = False def sort(self): eventDict = {} newEvents = [] for time, event in self.allEvents: if isinstance(event, VocalNote): eventDict[int(time)] = (time, event) times = eventDict.keys() times.sort() for time in times: newEvents.append(eventDict[time]) self.allEvents = newEvents class TempoTrack(Track): #MFH - special Track type for tempo events def __init__(self, engine): Track.__init__(self, engine) self.currentBpm = DEFAULT_BPM def reset(self): self.currentBpm = DEFAULT_BPM #MFH - function to track current tempo in realtime based on time / position def getCurrentTempo(self, pos): #MFH if self.currentIndex: tempEventHolder = self.getNextEvent() #MFH - check if next BPM change is here yet if tempEventHolder: time, event = tempEventHolder if pos >= time: self.currentIndex += 1 self.currentBpm = event.bpm return self.currentBpm def getNextTempoChange(self, pos): #MFH if self.currentIndex: #return self.getNextEvent(lookAhead = 1) return self.getNextEvent() return None def searchCurrentTempo(self, pos): #MFH - will hunt through all tempo events to find it - intended for use during initializations only! foundBpm = None foundTime = None for time, event in self.allEvents: if not foundBpm or not foundTime: foundBpm = event.bpm foundTime = time else: #MFH - want to discard if the foundTime is before pos, but this event is after pos. # -- also want to take newer BPM if time > foundTime >= pos if time <= pos: #MFH - first required condition. if time > foundTime: #MFH - second required condition for sorting. foundBpm = event.bpm foundTime = time if foundBpm: return foundBpm else: #MFH - return default BPM if no events return DEFAULT_BPM class NoteTrack(Track): #MFH - special Track type for note events, with marking functions def __init__(self, engine): Track.__init__(self, engine) self.chordFudge = 1 self.hopoTick = engine.config.get("coffee", "hopo_frequency") self.songHopoFreq = engine.config.get("game", "song_hopo_freq") self.logTempoEvents = engine.config.get("log", "log_tempo_events") def removeTempoEvents(self): for time, event in self.allEvents: if isinstance(event, Tempo): self.allEvents.remove((time, event)) if self.logTempoEvents == 1: Log.debug("Tempo event removed from NoteTrack during cleanup: " + str(event.bpm) + "bpm") def flipDrums(self): for time, event in self.allEvents: if isinstance(event, Note): event.number = (5-event.number)%5 def markHopoRF(self, EighthNH, songHopoFreq): lastTick = 0 lastTime = 0 lastEvent = Note tickDelta = 0 noteDelta = 0 try: songHopoFreq = int(songHopoFreq) except Exception, e: songHopoFreq = None # Log.warn("Song.ini HOPO Frequency setting is invalid -- forcing Normal (value 1)") if self.songHopoFreq == 1 and (songHopoFreq == 0 or songHopoFreq == 1 or songHopoFreq == 2 or songHopoFreq == 3 or songHopoFreq == 4 or songHopoFreq == 5): Log.debug("markHopoRF: song-specific HOPO frequency %d forced" % songHopoFreq) self.hopoTick = songHopoFreq #dtb file says 170 ticks hopoDelta = 170 if str(EighthNH) == "1": hopoDelta = 250 else: hopoDelta = 170 self.chordFudge = 10 if self.hopoTick == 0: ticksPerBeat = 960 elif self.hopoTick == 1: ticksPerBeat = 720 elif self.hopoTick == 2: ticksPerBeat = 480 elif self.hopoTick == 3: ticksPerBeat = 360 elif self.hopoTick == 4: ticksPerBeat = 240 else: ticksPerBeat = 240 hopoDelta = 250 hopoNotes = [] chordNotes = [] sameNotes = [] bpmNotes = [] firstTime = 1 #If already processed abort if self.marked == True: return for time, event in self.allEvents: if isinstance(event, Tempo): bpmNotes.append([time, event]) continue if not isinstance(event, Note): continue while bpmNotes and time >= bpmNotes[0][0]: #Adjust to new BPM #bpm = bpmNotes[0][1].bpm bpmTime, bpmEvent = bpmNotes.pop(0) bpm = bpmEvent.bpm tick = (time * bpm * ticksPerBeat) / 60000.0 lastTick = (lastTime * bpm * ticksPerBeat) / 60000.0 #skip first note if firstTime == 1: event.tappable = -3 lastEvent = event lastTime = time firstTime = 0 continue tickDelta = tick - lastTick noteDelta = event.number - lastEvent.number #previous note and current note HOPOable if tickDelta <= hopoDelta: #Add both notes to HOPO list even if duplicate. Will come out in processing if (not hopoNotes) or not (hopoNotes[-1][0] == lastTime and hopoNotes[-1][1] == lastEvent): #special case for first marker note. Change it to a HOPO start if not hopoNotes and lastEvent.tappable == -3: lastEvent.tappable = 1 #this may be incorrect if a bpm event happened inbetween this note and last note hopoNotes.append([lastTime, bpmEvent]) hopoNotes.append([lastTime, lastEvent]) hopoNotes.append([bpmTime, bpmEvent]) hopoNotes.append([time, event]) #HOPO Over if tickDelta > hopoDelta: if hopoNotes != []: #If the last event is the last HOPO note, mark it as a HOPO end if lastEvent.tappable != -1 and hopoNotes[-1][1] == lastEvent: if lastEvent.tappable >= 0: lastEvent.tappable = 3 else: lastEvent.tappable = -1 #This is the same note as before elif noteDelta == 0: #Add both notes to bad list even if duplicate. Will come out in processing sameNotes.append(lastEvent) sameNotes.append(event) lastEvent.tappable = -2 event.tappable = -2 #This is a chord elif tickDelta < self.chordFudge: #Add both notes to bad list even if duplicate. Will come out in processing if len(chordNotes) != 0 and chordNotes[-1] != lastEvent: chordNotes.append(lastEvent) chordNotes.append(event) lastEvent.tappable = -1 event.tappable = -1 lastEvent = event lastTime = time else: #Add last note to HOPO list if applicable if noteDelta != 0 and tickDelta > 1.5 and tickDelta < hopoDelta and isinstance(event, Note): hopoNotes.append([time, bpmEvent]) hopoNotes.append([time, event]) firstTime = 1 note = None for note in list(chordNotes): #chord notes -1 note.tappable = -1 for note in list(sameNotes): #same note in string -2 note.tappable = -2 bpmNotes = [] for time, note in list(hopoNotes): if isinstance(note, Tempo): bpmNotes.append([time, note]) continue if not isinstance(note, Note): continue while bpmNotes and time >= bpmNotes[0][0]: #Adjust to new BPM #bpm = bpmNotes[0][1].bpm bpmTime, bpmEvent = bpmNotes.pop(0) bpm = bpmEvent.bpm if firstTime == 1: if note.tappable >= 0: note.tappable = 1 lastEvent = note firstTime = 0 continue #need to recompute (or carry forward) BPM at this time tick = (time * bpm * ticksPerBeat) / 60000.0 lastTick = (lastTime * bpm * ticksPerBeat) / 60000.0 tickDelta = tick - lastTick #current Note Invalid if note.tappable < 0: #If current note is invalid for HOPO, and previous note was start of a HOPO section, then previous note not HOPO if lastEvent.tappable == 1: lastEvent.tappable = 0 #If current note is beginning of a same note sequence, it's valid for END of HOPO only elif lastEvent.tappable > 0: lastEvent.tappable = 3 #current note valid elif note.tappable >= 0: #String of same notes can be followed by HOPO if note.tappable == 3: #This is the end of a valid HOPO section if lastEvent.tappable == 1 or lastEvent.tappable == 2: lastEvent = note lastTime = time continue if lastEvent.tappable == -2: #If its the same note again it's invalid if lastEvent.number == note.number: note.tappable = -2 else: lastEvent.tappable = 1 elif lastEvent.tappable == 0: lastEvent.tappable = 1 #If last note was invalid or end of HOPO section, and current note is end, it is really not HOPO elif lastEvent.tappable != 2 and lastEvent.tappable != 1: note.tappable = 0 #If last event was invalid or end of HOPO section, current note is start of HOPO else: note.tappable = 1 elif note.tappable == 2: if lastEvent.tappable == -2: note.tappable = 1 elif lastEvent.tappable == -1: note.tappable = 0 elif note.tappable == 1: if lastEvent.tappable == 2: note.tappable = 0 else: if lastEvent.tappable == -2: if tickDelta <= hopoDelta: lastEvent.tappable = 1 if lastEvent.tappable != 2 and lastEvent.tappable != 1: note.tappable = 1 else: if note.tappable == 1: note.tappable = 1 else: note.tappable = 2 lastEvent = note lastTime = time else: if note != None: #Handle last note #If it is the start of a HOPO, it's not really a HOPO if note.tappable == 1: note.tappable = 0 #If it is the middle of a HOPO, it's really the end of a HOPO elif note.tappable == 2: note.tappable = 3 self.marked = True for time, event in self.allEvents: if isinstance(event, Tempo): bpmNotes.append([time, event]) continue if not isinstance(event, Note): continue #--------------------- def markHopoGH2(self, EighthNH, HoposAfterChords, songHopoFreq): lastTick = 0 lastTime = 0 lastEvent = Note tickDelta = 0 noteDelta = 0 try: songHopoFreq = int(songHopoFreq) except Exception, e: songHopoFreq = None # Log.warn("Song.ini HOPO Frequency setting is invalid -- forcing Normal (value 1)") if self.songHopoFreq == 1 and (songHopoFreq == 0 or songHopoFreq == 1 or songHopoFreq == 2 or songHopoFreq == 3 or songHopoFreq == 4 or songHopoFreq == 5): Log.debug("markHopoGH2: song-specific HOPO frequency %d forced" % songHopoFreq) self.hopoTick = songHopoFreq #dtb file says 170 ticks hopoDelta = 170 if str(EighthNH) == "1": hopoDelta = 250 else: hopoDelta = 170 #chordFudge = 10 self.chordFudge = 1 #MFH - there should be no chord fudge. if self.hopoTick == 0: ticksPerBeat = 960 elif self.hopoTick == 1: ticksPerBeat = 720 elif self.hopoTick == 2: ticksPerBeat = 480 elif self.hopoTick == 3: ticksPerBeat = 360 elif self.hopoTick == 4: ticksPerBeat = 240 else: ticksPerBeat = 240 hopoDelta = 250 hopoNotes = [] #myfingershurt: tickDeltaBeforeLast = 0 #3 notes in the past lastTickDelta = 0 #2 notes in the past bpmNotes = [] firstTime = 1 #MFH - to prevent crashes on songs without a BPM set bpmEvent = None bpm = None #If already processed abort if self.marked == True: return for time, event in self.allEvents: if isinstance(event, Tempo): bpmNotes.append([time, event]) continue if not isinstance(event, Note): continue while bpmNotes and time >= bpmNotes[0][0]: #Adjust to new BPM #bpm = bpmNotes[0][1].bpm bpmTime, bpmEvent = bpmNotes.pop(0) bpm = bpmEvent.bpm if not bpm: bpm = DEFAULT_BPM tick = (time * bpm * ticksPerBeat) / 60000.0 lastTick = (lastTime * bpm * ticksPerBeat) / 60000.0 #skip first note if firstTime == 1: event.tappable = -3 lastEvent = event lastTime = time eventBeforeLast = lastEvent timeBeforeLast = lastTime eventBeforeEventBeforeLast = eventBeforeLast timeBeforeTimeBeforeLast = timeBeforeLast firstTime = 0 continue #tickDeltaBeforeTickDeltaBeforeLast = tickDeltaBeforeLast #4 notes in the past tickDeltaBeforeLast = lastTickDelta #3 notes in the past lastTickDelta = tickDelta #2 notes in the past tickDelta = tick - lastTick noteDelta = event.number - lastEvent.number #myfingershurt: This initial sweep drops any notes within the timing # threshold into the hopoNotes array (which would be more aptly named, # the "potentialHopoNotes" array). Another loop down below # further refines the HOPO determinations... #previous note and current note HOPOable if tickDelta <= hopoDelta: #Add both notes to HOPO list even if duplicate. Will come out in processing if (not hopoNotes) or not (hopoNotes[-1][0] == lastTime and hopoNotes[-1][1] == lastEvent): #special case for first marker note. Change it to a HOPO start if not hopoNotes and lastEvent.tappable == -3: lastEvent.tappable = 1 #this may be incorrect if a bpm event happened inbetween this note and last note if bpmEvent: hopoNotes.append([lastTime, bpmEvent]) hopoNotes.append([lastTime, lastEvent]) if bpmEvent: hopoNotes.append([bpmTime, bpmEvent]) hopoNotes.append([time, event]) #HOPO definitely over - time since last note too great. if tickDelta > hopoDelta: if hopoNotes != []: #If the last event is the last HOPO note, mark it as a HOPO end if lastEvent.tappable != -1 and hopoNotes[-1][1] == lastEvent: if lastEvent.tappable >= 0: lastEvent.tappable = 3 else: lastEvent.tappable = -1 #note pattern: # R R R R R # Y Y #should be # 1 3-2-2-4 #first pass: #actual: if first RY considered "same note" as next R: #-4 0-2-2 #if last RY considered "same note" as prev R: #-4 0-2-2-2 #actual: if first RY not considered "same note" as next R: #-4 0-2-2 #This is the same note as before elif noteDelta == 0: #Add both notes to bad list even if duplicate. Will come out in processing #myfingershurt: to fix same-note HOPO bug. if HoposAfterChords: if lastEvent.tappable != -4: #if the last note was not a chord, if eventBeforeLast.tappable == -4 and lastTickDelta <= hopoDelta: #and if the event before last was a chord, don't remark the last one. event.tappable = -5 else: event.tappable = -2 #myfingershurt: yeah, mark the last one. lastEvent.tappable = -2 else: event.tappable = -2 lastEvent.tappable = -2 #This is a chord #myfingershurt: both this note and the last note are listed at the same time #to allow after-chord HOPOs, we need a separate identification for "chord" notes #and also might need to track the "last" chord notes in a separate array elif tickDelta < self.chordFudge: #Add both notes to bad list even if duplicate. Will come out in processing #myfingershurt: if HoposAfterChords: if eventBeforeLast.tappable == -2 and lastTickDelta <= hopoDelta: if eventBeforeEventBeforeLast.tappable >= 0 and tickDeltaBeforeLast <= hopoDelta: eventBeforeLast.tappable = -5 #special case that needs to be caught if lastEvent.tappable == -2: #catch case where event before last event was marked as the same note if eventBeforeEventBeforeLast.tappable >= 0 and tickDeltaBeforeLast <= hopoDelta: eventBeforeLast.tappable = 0 lastEvent.tappable = -4 event.tappable = -4 #keep track of chords as they are found else: lastEvent.tappable = -1 event.tappable = -1 #myfingershurt: to really check marking, need to track 3 notes into the past. eventBeforeEventBeforeLast = eventBeforeLast timeBeforeTimeBeforeLast = timeBeforeLast eventBeforeLast = lastEvent timeBeforeLast = lastTime lastEvent = event lastTime = time else: #Add last note to HOPO list if applicable #myfingershurt: if noteDelta != 0 and tickDelta > self.chordFudge and tickDelta < hopoDelta and isinstance(event, Note): if bpmEvent: hopoNotes.append([time, bpmEvent]) hopoNotes.append([time, event]) #myfingershurt marker: (next - FOR loop)---- #myfingershurt: comments - updated marking system #--at this point, the initial note marking sweep has taken place. Here is further marking of hopoNotes. #the .tappable property has special meanings: # -5 = This note is the same as the last note, which was a HOPO, and the next note is a chord. # or, this note is the same as the last note, which was a HOPO, so no matter what it shouldn't be tappable. # -4 = This note is part of a chord # -3 = This is the very first note in a song # -2 = This note is part of a string of "same notes" # -1 = This note is too far away from the last note to be tappable. # 0 = This note is nowhere near another note and must be strummed (also default value) # 1 = This note is the first in a string of HOPOs - this note must be strummed to initiate the HOPOs following it! # 2 = This note is in the midst of HOPOs # 3 = This note is the final of a string of HOPOs firstTime = 1 note = None tickDeltaBeforeTickDeltaBeforeLast = 0 #4 notes in the past tickDeltaBeforeLast = 0 #3 notes in the past lastTickDelta = 0 #2 notes in the past bpmNotes = [] for time, note in list(hopoNotes): if isinstance(note, Tempo): bpmNotes.append([time, note]) continue if not isinstance(note, Note): continue while bpmNotes and time >= bpmNotes[0][0]: #Adjust to new BPM #bpm = bpmNotes[0][1].bpm bpmTime, bpmEvent = bpmNotes.pop(0) bpm = bpmEvent.bpm if firstTime == 1: if note.tappable >= 0: note.tappable = 1 lastEvent = note lastTime = time firstTime = 0 eventBeforeLast = lastEvent eventBeforeEventBeforeLast = eventBeforeLast eventBeforeEventBeforeEventBeforeLast = eventBeforeLast continue #need to recompute (or carry forward) BPM at this time tickDeltaBeforeTickDeltaBeforeLast = tickDeltaBeforeLast #4 notes in the past tickDeltaBeforeLast = lastTickDelta #3 notes in the past lastTickDelta = tickDelta #2 notes in the past tick = (time * bpm * ticksPerBeat) / 60000.0 lastTick = (lastTime * bpm * ticksPerBeat) / 60000.0 tickDelta = tick - lastTick #current Note Invalid for HOPO---------- if note.tappable < 0: #test note pattern: # R R R R R R # Y Y Y Y Y Y #first pass: #-4-2-4-2-5-2-2-4 #second pass: # 1 3 1 3 1 3-2-4 #myfingershurt: #If current note is beginning of a same note sequence, it's valid for END of HOPO only #need to alter this to not screw up single-same-note-before chords: #elif lastEvent.tappable == 2 and note.tappable == -2: if (lastEvent.tappable == 1 or lastEvent.tappable == 2) and note.tappable == -2: note.tappable = 3 #If current note is invalid for HOPO, and previous note was start of a HOPO section, then previous note not HOPO elif lastEvent.tappable == 1: lastEvent.tappable = 0 #and let's not forget the special case when two same-note-sequences on different frets are in sequence: elif lastEvent.tappable == -2 and note.tappable == -2 and lastEvent.number != note.number: lastEvent.tappable = 1 note.tappable = 3 #special case where the new -5 note is followed by a string same notes on a different fret: elif lastEvent.tappable == -5 and note.tappable == -2: lastEvent.tappable = 1 note.tappable = 3 #special case where last note was a chord, and this note is counted as the same note as the last in the chord elif lastEvent.tappable == -4 and note.tappable == -2: thisNoteShouldStillBeAHopo = True #determine the highest note in the chord: topChordNote = lastEvent.number if eventBeforeLast.number > topChordNote: topChordNote = eventBeforeLast.number numChordNotes = 2 #now to determine how many notes were in that chord: if tickDeltaBeforeLast < self.chordFudge: #is there a 3rd note in the same chord? if eventBeforeEventBeforeLast.number > topChordNote: topChordNote = eventBeforeEventBeforeLast.number numChordNotes = 3 if tickDeltaBeforeTickDeltaBeforeLast < self.chordFudge: #is there a 4th note in the same chord? if eventBeforeEventBeforeEventBeforeLast.number > topChordNote: topChordNote = eventBeforeEventBeforeEventBeforeLast.number numChordNotes = 4 if topChordNote == note.number: #only if the note number matches the top chord note do we outlaw the HOPO. thisNoteShouldStillBeAHopo = False if thisNoteShouldStillBeAHopo: #here, need to go back and mark all notes in this chord as tappable = 1 not just the last one. #chord max of 4 frets (5 is just ridiculous) - so if "lastEvent" is the 4th, must go back and change 3rd, 2nd, and 1st lastEvent.tappable = 1 eventBeforeLast.tappable = 1 #at least 2 notes in chord if numChordNotes >= 3: #is there a 3rd note in the same chord? eventBeforeEventBeforeLast.tappable = 1 if numChordNotes == 4: #is there a 4th note in the same chord? eventBeforeEventBeforeEventBeforeLast.tappable = 1 note.tappable = 3 #If current note is invalid for HOPO, and previous note was a HOPO section, then previous note is end of HOPO elif lastEvent.tappable > 0: lastEvent.tappable = 3 #current note valid elif note.tappable >= 0: #String of same notes can be followed by HOPO if note.tappable == 3: #myfingershurt: the new after-chord HOPO special logic needs to also be applied here if lastEvent.tappable == -5: #special case, still can be HOPO start lastEvent.tappable = 1 if lastEvent.tappable == -4: #chord thisNoteShouldStillBeAHopo = True #determine the highest note in the chord: topChordNote = lastEvent.number if eventBeforeLast.number > topChordNote: topChordNote = eventBeforeLast.number numChordNotes = 2 #now to determine how many notes were in that chord: if tickDeltaBeforeLast < self.chordFudge: #is there a 3rd note in the same chord? if eventBeforeEventBeforeLast.number > topChordNote: topChordNote = eventBeforeEventBeforeLast.number numChordNotes = 3 if tickDeltaBeforeTickDeltaBeforeLast < self.chordFudge: #is there a 4th note in the same chord? if eventBeforeEventBeforeEventBeforeLast.number > topChordNote: topChordNote = eventBeforeEventBeforeEventBeforeLast.number numChordNotes = 4 if topChordNote == note.number: #only if the note number matches the top chord note do we outlaw the HOPO. thisNoteShouldStillBeAHopo = False if thisNoteShouldStillBeAHopo: #here, need to go back and mark all notes in this chord as tappable = 1 not just the last one. #chord max of 4 frets (5 is just ridiculous) - so if "lastEvent" is the 4th, must go back and change 3rd, 2nd, and 1st lastEvent.tappable = 1 eventBeforeLast.tappable = 1 #at least 2 notes in chord if numChordNotes >= 3: #is there a 3rd note in the same chord? eventBeforeEventBeforeLast.tappable = 1 if numChordNotes == 4: #is there a 4th note in the same chord? eventBeforeEventBeforeEventBeforeLast.tappable = 1 else: note.tappable = -1 #This is the end of a valid HOPO section if lastEvent.tappable == 1 or lastEvent.tappable == 2: eventBeforeEventBeforeEventBeforeLast = eventBeforeLast #timeBeforeTimeBeforeTimeBeforeLast = timeBeforeLast eventBeforeEventBeforeLast = eventBeforeLast #timeBeforeTimeBeforeLast = timeBeforeLast eventBeforeLast = lastEvent #timeBeforeLast = lastTime lastEvent = note lastTime = time continue if lastEvent.tappable == -2: #If its the same note again it's invalid if lastEvent.number == note.number: note.tappable = -2 else: lastEvent.tappable = 1 elif lastEvent.tappable == 0: lastEvent.tappable = 1 #If last note was invalid or end of HOPO section, and current note is end, it is really not HOPO elif lastEvent.tappable != 2 and lastEvent.tappable != 1: note.tappable = 0 #If last event was invalid or end of HOPO section, current note is start of HOPO else: note.tappable = 1 elif note.tappable == 2: if lastEvent.tappable == -2: note.tappable = 1 elif lastEvent.tappable == -1: note.tappable = 0 elif lastEvent.tappable == 3: lastEvent.tappable = 2 elif note.tappable == 1: if lastEvent.tappable == 2: note.tappable = 0 #myfingershurt: default case, tappable=0 else: if lastEvent.tappable == -5: #special case, still can be HOPO start lastEvent.tappable = 1 if lastEvent.tappable == -4: #chord thisNoteShouldStillBeAHopo = True #determine the highest note in the chord: topChordNote = lastEvent.number if eventBeforeLast.number > topChordNote: topChordNote = eventBeforeLast.number numChordNotes = 2 #now to determine how many notes were in that chord: if tickDeltaBeforeLast < self.chordFudge: #is there a 3rd note in the same chord? if eventBeforeEventBeforeLast.number > topChordNote: topChordNote = eventBeforeEventBeforeLast.number numChordNotes = 3 if tickDeltaBeforeTickDeltaBeforeLast < self.chordFudge: #is there a 4th note in the same chord? if eventBeforeEventBeforeEventBeforeLast.number > topChordNote: topChordNote = eventBeforeEventBeforeEventBeforeLast.number numChordNotes = 4 if topChordNote == note.number: #only if the note number matches the top chord note do we outlaw the HOPO. thisNoteShouldStillBeAHopo = False if thisNoteShouldStillBeAHopo: #here, need to go back and mark all notes in this chord as tappable = 1 not just the last one. #chord max of 4 frets (5 is just ridiculous) - so if "lastEvent" is the 4th, must go back and change 3rd, 2nd, and 1st lastEvent.tappable = 1 eventBeforeLast.tappable = 1 #at least 2 notes in chord if numChordNotes >= 3: #is there a 3rd note in the same chord? eventBeforeEventBeforeLast.tappable = 1 if numChordNotes == 4: #is there a 4th note in the same chord? eventBeforeEventBeforeEventBeforeLast.tappable = 1 else: note.tappable = -1 if lastEvent.tappable == -2 and tickDelta <= hopoDelta: #same note lastEvent.tappable = 1 if lastEvent.tappable == 3 and tickDelta <= hopoDelta: #supposedly last of a HOPO lastEvent.tappable = 2 if lastEvent.tappable != 2 and lastEvent.tappable != 1: note.tappable = 1 else: if note.tappable == 1: note.tappable = 1 else: note.tappable = 2 #myfingershurt: to really check marking, need to track 4 notes into the past. eventBeforeEventBeforeEventBeforeLast = eventBeforeLast eventBeforeEventBeforeLast = eventBeforeLast eventBeforeLast = lastEvent lastEvent = note lastTime = time else: if note != None: #Handle last note #If it is the start of a HOPO, it's not really a HOPO if note.tappable == 1: note.tappable = 0 #If it is the middle of a HOPO, it's really the end of a HOPO elif note.tappable == 2: #myfingershurt: here, need to check if the last note is a HOPO after chord and is being merged into the chord (which happens if too close to end of song) if tickDelta < self.chordFudge: note.tappable = -4 else: note.tappable = 3 self.marked = True def markBars(self): tempoTime = [] tempoBpm = [] #get all the bpm changes and their times #MFH - TODO - count tempo events. If 0, realize and log that this is a song with no tempo events - and go mark all 120BPM bars. endBpm = None endTime = None for time, event in self.allEvents: if isinstance(event, Tempo): tempoTime.append(time) tempoBpm.append(event.bpm) endBpm = event.bpm continue if isinstance(event, Note): endTime = time + event.length + 30000 continue if endTime: tempoTime.append(endTime) if endBpm: tempoBpm.append(endBpm) #calculate and add the measures/beats/half-beats passes = 0 limit = len(tempoTime) time = tempoTime[0] THnote = 256.0 #256th note drawBar = True i = 0 while i < (limit - 1): msTotal = tempoTime[i+1] - time if msTotal == 0: i += 1 continue tempbpm = tempoBpm[i] nbars = (msTotal * (tempbpm / (240.0 / THnote) )) / 1000.0 inc = msTotal / nbars while time < tempoTime[i+1]: if drawBar == True: if (passes % (THnote / 1.0) == 0.0): #256/1 event = Bars(2) #measure self.addEvent(time, event) elif (passes % (THnote / 4.0) == 0.0): #256/4 event = Bars(1) #beat self.addEvent(time, event) elif (passes % (THnote / 8.0) == 0.0): #256/8 event = Bars(0) #half-beat self.addEvent(time, event) passes = passes + 1 time = time + inc drawBar = True if time > tempoTime[i+1]: time = time - inc drawBar = False i += 1 #add the last measure/beat/half-beat if time == tempoTime[i]: if (passes % (THnote / 1.0) == 0.0): #256/1 event = Bars(2) #measure self.addEvent(time, event) elif (passes % (THnote / 4.0) == 0.0): #256/4 event = Bars(1) #beat self.addEvent(time, event) elif (passes % (THnote / 8.0) == 0.0): #256/8 event = Bars(0) #half-beat self.addEvent(time, event) class Song(object): def __init__(self, engine, infoFileName, songTrackName, guitarTrackName, rhythmTrackName, noteFileName, scriptFileName = None, partlist = [parts[GUITAR_PART]], drumTrackName = None, crowdTrackName = None): self.engine = engine self.logClassInits = self.engine.config.get("game", "log_class_inits") if self.logClassInits == 1: Log.debug("Song class init (song.py)...") self.info = SongInfo(infoFileName) self.tracks = [] for i in partlist: if i == parts[VOCAL_PART]: self.tracks.append([VocalTrack(self.engine)]) else: self.tracks.append([NoteTrack(self.engine) for t in range(len(difficulties))]) self.difficulty = [difficulties[EXP_DIF] for i in partlist] self._playing = False self.start = 0.0 self.noteFileName = noteFileName #self.bpm = None self.bpm = DEFAULT_BPM #MFH - enforcing a default / fallback tempo of 120 BPM #self.period = 0 self.period = 60000.0 / self.bpm #MFH - enforcing a default / fallback tempo of 120 BPM self.readyToGo = False #MFH - to prevent anything from happening until all prepration & initialization is complete! self.parts = partlist self.delay = self.engine.config.get("audio", "delay") self.delay += self.info.delay self.missVolume = self.engine.config.get("audio", "miss_volume") self.backVolume = self.engine.config.get("audio", "songvol") #akedrou self.activeVolume = self.engine.config.get("audio", "guitarvol") self.crowdVolume = self.engine.config.get("audio", "crowd_volume") self.hasMidiLyrics = False self.midiStyle = self.info.midiStyle self.earlyHitWindowSize = EARLY_HIT_WINDOW_HALF #MFH - holds the detected early hit window size for the current song self.hasStarpowerPaths = False self.hasFreestyleMarkings = False #MFH - add a separate variable to house the special text event tracks: #MFH - special text-event tracks for the separated text-event list variable #TK_SCRIPT = 0 #script.txt events #TK_SECTIONS = 1 #Section events #TK_GUITAR_SOLOS = 2 #Guitar Solo start/stop events #TK_LYRICS = 3 #RB MIDI Lyric events #TK_UNUSED_TEXT = 4 #Unused / other text events self.eventTracks = [Track(self.engine) for t in range(0,5)] #MFH - should result in eventTracks[0] through [4] self.midiEventTracks = [] self.activeAudioTracks = [] for i in partlist: if i == parts[VOCAL_PART]: self.midiEventTracks.append([None]) else: self.midiEventTracks.append([Track(self.engine) for t in range(len(difficulties))]) if i == parts[GUITAR_PART] or i == parts[LEAD_PART]: self.activeAudioTracks.append(GUITAR_TRACK) elif i == parts[RHYTHM_PART] or i == parts[BASS_PART]: self.activeAudioTracks.append(RHYTHM_TRACK) elif i == parts[DRUM_PART]: self.activeAudioTracks.append(DRUM_TRACK) self.vocalEventTrack = VocalTrack(self.engine) self.tempoEventTrack = TempoTrack(self.engine) #MFH - need a separated Tempo/BPM track! self.breMarkerTime = None self.music = None #self.slowDownFactor = self.engine.config.get("audio", "speed_factor") #self.engine.setSpeedFactor(self.slowDownFactor) #MFH # load the tracks #if self.engine.audioSpeedFactor == 1: #MFH - only use the music track if songTrackName: self.music = Audio.Music(songTrackName) self.guitarTrack = None self.rhythmTrack = None #myfingershurt: self.drumTrack = None #akedrou self.crowdTrack = None #if songTrackName: # drumTrackName = songTrackName.replace("song.ogg","drums.ogg") try: if guitarTrackName: self.guitarTrack = Audio.StreamingSound(self.engine, self.engine.audio.getChannel(1), guitarTrackName) except Exception, e: Log.warn("Unable to load guitar track: %s" % e) try: if rhythmTrackName: self.rhythmTrack = Audio.StreamingSound(self.engine, self.engine.audio.getChannel(2), rhythmTrackName) except Exception, e: Log.warn("Unable to load rhythm track: %s" % e) try: if drumTrackName: self.drumTrack = Audio.StreamingSound(self.engine, self.engine.audio.getChannel(3), drumTrackName) except Exception, e: Log.warn("Unable to load drum track: %s" % e) try: if crowdTrackName: self.crowdTrack = Audio.StreamingSound(self.engine, self.engine.audio.getChannel(4), crowdTrackName) except Exception, e: Log.warn("Unable to load crowd track: %s" % e) #MFH - single audio track song detection self.singleTrackSong = False if (self.music == None) or (self.guitarTrack == None and self.rhythmTrack == None and self.drumTrack == None): if not (self.music == None and self.guitarTrack == None and self.rhythmTrack == None and self.drumTrack == None): self.singleTrackSong = True self.missVolume = self.engine.config.get("audio", "single_track_miss_volume") #MFH - force single-track miss volume setting instead Log.debug("Song with only a single audio track identified - single-track miss volume applied: " + str(self.missVolume)) # load the notes if noteFileName: Log.debug("Retrieving notes from: " + noteFileName) midiIn = midi.MidiInFile(MidiReader(self), noteFileName) midiIn.read() # load the script if scriptFileName and os.path.isfile(scriptFileName): scriptReader = ScriptReader(self, open(scriptFileName)) scriptReader.read() #myfingershurt: new function to re-retrieve the a/v delay setting so it can be changed in-game: def refreshAudioDelay(self): self.delay = self.engine.config.get("audio", "delay") self.delay += self.info.delay #myfingershurt: new function to refresh the miss volume after a pause def refreshVolumes(self): if self.singleTrackSong: self.missVolume = self.engine.config.get("audio", "single_track_miss_volume") #MFH - force single-track miss volume setting instead else: self.missVolume = self.engine.config.get("audio", "miss_volume") self.activeVolume = self.engine.config.get("audio", "guitarvol") self.backVolume = self.engine.config.get("audio", "songvol") def getCurrentTempo(self, pos): #MFH return self.tempoEventTrack.getCurrentTempo(pos) def getHash(self): h = hashlib.sha1() f = open(self.noteFileName, "rb") bs = 1024 while 1: data = f.read(bs) if not data: break h.update(data) return h.hexdigest() def setBpm(self, bpm): self.bpm = bpm self.period = 60000.0 / self.bpm def save(self): self.info.save() f = open(self.noteFileName + ".tmp", "wb") midiOut = MidiWriter(self, midi.MidiOutFile(f)) midiOut.write() f.close() # Rename the output file after it has been succesfully written shutil.move(self.noteFileName + ".tmp", self.noteFileName) def play(self, start = 0.0): self.start = start #RF-mod No longer needed? if self.engine.audioSpeedFactor == 1: #MFH - shut this track up if slowing audio down! self.music.play(0, start / 1000.0) if self.singleTrackSong: self.music.setVolume(self.activeVolume) else: self.music.setVolume(self.backVolume) else: self.music.play(int(math.ceil(1.0/self.engine.audioSpeedFactor)), start / 1000.0) #tell music to loop 2 times for 1/2 speed, 4 times for 1/4 speed (so it doesn't end early) (it wants an int, though) self.music.setVolume(0.0) if self.guitarTrack: assert start == 0.0 self.guitarTrack.play() if self.rhythmTrack: assert start == 0.0 self.rhythmTrack.play() #myfingershurt if self.drumTrack: assert start == 0.0 self.drumTrack.play() if self.crowdTrack: assert start == 0.0 self.crowdTrack.play() self._playing = True def pause(self): self.music.pause() self.engine.audio.pause() def unpause(self): self.music.unpause() self.engine.audio.unpause() def setInstrumentVolume(self, volume, part): if self.singleTrackSong: self.setGuitarVolume(volume) elif part == parts[GUITAR_PART]: self.setGuitarVolume(volume) # elif part == parts[BASS_PART]: # self.setRhythmVolume(volume) # elif part == parts[RHYTHM_PART]: # self.setRhythmVolume(volume) elif part == parts[DRUM_PART]: self.setDrumVolume(volume) else: self.setRhythmVolume(volume) def setGuitarVolume(self, volume): if self.guitarTrack: if volume == 0: self.guitarTrack.setVolume(self.missVolume) elif volume == 1: if GUITAR_TRACK in self.activeAudioTracks or self.singleTrackSong: self.guitarTrack.setVolume(self.activeVolume) else: self.guitarTrack.setVolume(self.backVolume) else: self.guitarTrack.setVolume(volume) #This is only used if there is no guitar.ogg to lower the volume of song.ogg instead of muting this track # evilynux - It also falls on this with buggy pygame < 1.9 on 64bit CPUs. else: if volume == 0: self.music.setVolume(self.missVolume) elif volume == 1: if GUITAR_TRACK in self.activeAudioTracks or self.singleTrackSong: self.music.setVolume(self.activeVolume) else: self.music.setVolume(self.backVolume) else: self.music.setVolume(volume) def setRhythmVolume(self, volume): if self.rhythmTrack: if volume == 0: self.rhythmTrack.setVolume(self.missVolume) elif volume == 1: if RHYTHM_TRACK in self.activeAudioTracks: self.rhythmTrack.setVolume(self.activeVolume) else: self.rhythmTrack.setVolume(self.backVolume) else: self.rhythmTrack.setVolume(volume) def setDrumVolume(self, volume): if self.drumTrack: if volume == 0: self.drumTrack.setVolume(self.missVolume) elif volume == 1: if DRUM_TRACK in self.activeAudioTracks: self.drumTrack.setVolume(self.activeVolume) else: self.drumTrack.setVolume(self.backVolume) else: self.drumTrack.setVolume(volume) #stump: pitch bending def setInstrumentPitch(self, pitch, part): if part == parts[GUITAR_PART]: if self.guitarTrack: self.guitarTrack.setPitchBend(pitch) else: self.music.setPitchBend(pitch) elif part == parts[DRUM_PART]: pass else: if self.rhythmTrack: self.rhythmTrack.setPitchBend(pitch) def resetInstrumentPitch(self, part): if part == -1: #this is just a convenient way to ensure we grab the 'music' channel when resetting pitch (rather; reset bending on all output) self.music.stopPitchBend() elif part == parts[GUITAR_PART]: if self.guitarTrack: self.guitarTrack.stopPitchBend() else: self.music.stopPitchBend() elif part == parts[DRUM_PART]: pass else: if self.rhythmTrack: self.rhythmTrack.stopPitchBend() def setBackgroundVolume(self, volume): if volume == 1: self.music.setVolume(self.backVolume) else: self.music.setVolume(volume) def setCrowdVolume(self, volume): if self.crowdTrack: if volume == 1: self.crowdTrack.setVolume(self.crowdVolume) else: self.crowdTrack.setVolume(volume) def setAllTrackVolumes(self, volume): self.setBackgroundVolume(volume) self.setDrumVolume(volume) self.setRhythmVolume(volume) self.setGuitarVolume(volume) self.setCrowdVolume(volume) def stop(self): for tracks in self.tracks: for track in tracks: track.reset() for tracks in self.midiEventTracks: for track in tracks: if track is not None: track.reset() if self.music: #MFH self.music.stop() self.music.rewind() if self.guitarTrack: self.guitarTrack.stop() if self.rhythmTrack: self.rhythmTrack.stop() if self.drumTrack: self.drumTrack.stop() if self.crowdTrack: self.crowdTrack.stop() self._playing = False def fadeout(self, time): for tracks in self.tracks: for track in tracks: track.reset() self.music.fadeout(time) if self.guitarTrack: self.guitarTrack.fadeout(time) if self.rhythmTrack: self.rhythmTrack.fadeout(time) if self.drumTrack: self.drumTrack.fadeout(time) if self.crowdTrack: self.crowdTrack.fadeout(time) self._playing = False def clearPause(self): self.music.isPause = False self.music.toUnpause = False self.music.pausePos = 0.0 def getPosition(self): if not self._playing: pos = 0.0 else: pos = self.music.getPosition() if self.engine.audioSpeedFactor != 1: #MFH - to correct for slowdown's positioning #pos /= self.engine.audioSpeedFactor pos *= self.engine.audioSpeedFactor if pos < 0.0: pos = 0.0 return pos + self.start - self.delay def isPlaying(self): #MFH - check here to see if any audio tracks are still playing first! #MFH from the Future sez: but only if in slowdown mode! if self.engine.audioSpeedFactor == 1: return self._playing and self.music.isPlaying() else: #altered speed mode! if self.guitarTrack and self.guitarTrack.streamIsPlaying() > 0: return True if self.rhythmTrack and self.rhythmTrack.streamIsPlaying() > 0: return True if self.drumTrack and self.drumTrack.streamIsPlaying() > 0: return True else: return self._playing and self.music.isPlaying() def getBeat(self): return self.getPosition() / self.period def update(self, ticks): pass def getTrack(self): return [self.tracks[i][self.difficulty[i].id] for i in range(len(self.difficulty))] def getIsSingleAudioTrack(self): return self.singleTrackSong track = property(getTrack) isSingleAudioTrack = property(getIsSingleAudioTrack) def getMidiEventTrack(self): #MFH - for new special MIDI marker note track return [self.midiEventTracks[i][self.difficulty[i].id] for i in range(len(self.difficulty))] midiEventTrack = property(getMidiEventTrack) #MFH - translating / marking the common MIDI notes: noteMap = { # difficulty, note 0x60: (EXP_DIF, 0), #=========#0x60 = 96 = C 8 0x61: (EXP_DIF, 1), #0x61 = 97 = Db8 0x62: (EXP_DIF, 2), #0x62 = 98 = D 8 0x63: (EXP_DIF, 3), #0x63 = 99 = Eb8 0x64: (EXP_DIF, 4), #0x64 = 100= E 8 0x54: (HAR_DIF, 0), #=========#0x54 = 84 = C 7 0x55: (HAR_DIF, 1), #0x55 = 85 = Db7 0x56: (HAR_DIF, 2), #0x56 = 86 = D 7 0x57: (HAR_DIF, 3), #0x57 = 87 = Eb7 0x58: (HAR_DIF, 4), #0x58 = 88 = E 7 0x48: (MED_DIF, 0), #=========#0x48 = 72 = C 6 0x49: (MED_DIF, 1), #0x49 = 73 = Db6 0x4a: (MED_DIF, 2), #0x4a = 74 = D 6 0x4b: (MED_DIF, 3), #0x4b = 75 = Eb6 0x4c: (MED_DIF, 4), #0x4c = 76 = E 6 0x3c: (EAS_DIF, 0), #=========#0x3c = 60 = C 5 0x3d: (EAS_DIF, 1), #0x3d = 61 = Db5 0x3e: (EAS_DIF, 2), #0x3e = 62 = D 5 0x3f: (EAS_DIF, 3), #0x3f = 63 = Eb5 0x40: (EAS_DIF, 4), #0x40 = 64 = E 5 } #MFH - special note numbers starPowerMarkingNote = 103 #note 103 = G 8 overDriveMarkingNote = 116 #note 116 = G#9 freestyleMarkingNote = 124 #notes 120 - 124 = drum fills & BREs - always all 5 notes present reverseNoteMap = dict([(v, k) for k, v in noteMap.items()]) class MidiWriter: def __init__(self, song, out): self.song = song self.out = out self.ticksPerBeat = 480 self.logClassInits = Config.get("game", "log_class_inits") if self.logClassInits == 1: Log.debug("MidiWriter class init (song.py)...") def midiTime(self, time): return int(self.song.bpm * self.ticksPerBeat * time / 60000.0) def write(self): self.out.header(division = self.ticksPerBeat) self.out.start_of_track() self.out.update_time(0) if self.song.bpm: self.out.tempo(int(60.0 * 10.0**6 / self.song.bpm)) else: self.out.tempo(int(60.0 * 10.0**6 / 122.0)) # Collect all events events = [zip([difficulty] * len(track.getAllEvents()), track.getAllEvents()) for difficulty, track in enumerate(self.song.tracks[0])] events = reduce(lambda a, b: a + b, events) events.sort(lambda a, b: {True: 1, False: -1}[a[1][0] > b[1][0]]) heldNotes = [] for difficulty, event in events: time, event = event if isinstance(event, Note): time = self.midiTime(time) # Turn off any held notes that were active before this point in time for note, endTime in list(heldNotes): if endTime <= time: self.out.update_time(endTime, relative = 0) self.out.note_off(0, note) heldNotes.remove((note, endTime)) note = reverseNoteMap[(difficulty, event.number)] self.out.update_time(time, relative = 0) self.out.note_on(0, note, event.special and 127 or 100) heldNotes.append((note, time + self.midiTime(event.length))) heldNotes.sort(lambda a, b: {True: 1, False: -1}[a[1] > b[1]]) # Turn off any remaining notes for note, endTime in heldNotes: self.out.update_time(endTime, relative = 0) self.out.note_off(0, note) self.out.update_time(0) self.out.end_of_track() self.out.eof() self.out.write() class ScriptReader: def __init__(self, song, scriptFile): self.song = song self.file = scriptFile self.logClassInits = Config.get("game", "log_class_inits") if self.logClassInits == 1: Log.debug("ScriptReader class init (song.py)...") def read(self): for line in self.file: if line.startswith("#") or line.isspace(): continue time, length, type, data = re.split("[\t ]+", line.strip(), 3) time = float(time) length = float(length) if type == "text": event = TextEvent(data, length) elif type == "pic": event = PictureEvent(data, length) else: continue self.song.eventTracks[TK_SCRIPT].addEvent(time, event) #MFH - add an event to the script.txt track #for track in self.song.tracks: # for t in track: # t.addEvent(time, event) class MidiReader(midi.MidiOutStream): def __init__(self, song): midi.MidiOutStream.__init__(self) self.song = song self.heldNotes = {} self.velocity = {} self.ticksPerBeat = 480 self.tempoMarkers = [] self.partTrack = 0 self.partnumber = -1 self.vocalTrack = False self.useVocalTrack = False self.vocalOverlapCheck = [] self.logClassInits = Config.get("game", "log_class_inits") if self.logClassInits == 1: Log.debug("MidiReader class init (song.py)...") self.logMarkerNotes = Config.get("game", "log_marker_notes") self.logTempoEvents = Config.get("log", "log_tempo_events") self.logSections = Config.get("game", "log_sections") self.readTextAndLyricEvents = Config.get("game","rock_band_events") self.guitarSoloIndex = 0 self.guitarSoloActive = False self.guitarSoloSectionMarkers = False def addEvent(self, track, event, time = None): if self.partnumber == -1: #Looks like notes have started appearing before any part information. Lets assume its part0 self.partnumber = self.song.parts[0] if (self.partnumber == None) and isinstance(event, Note): return True if time is None: time = self.abs_time() assert time >= 0 if track is None: for t in self.song.tracks: for s in t: s.addEvent(time, event) else: tracklist = [i for i,j in enumerate(self.song.parts) if self.partnumber == j] for i in tracklist: #Each track needs it's own copy of the event, otherwise they'll interfere eventcopy = copy.deepcopy(event) if track < len(self.song.tracks[i]): self.song.tracks[i][track].addEvent(time, eventcopy) def addVocalEvent(self, event, time = None): if time is None: time = self.abs_time() assert time >= 0 if not self.useVocalTrack: return True track = [i for i,j in enumerate(self.song.parts) if self.partnumber == j][0] if isinstance(event, VocalNote): self.song.track[track].minPitch = min(event.note, self.song.track[track].minPitch) self.song.track[track].maxPitch = max(event.note, self.song.track[track].maxPitch) self.song.track[track].allNotes[int(time)] = (time, event) elif isinstance(event, VocalPhrase): self.song.track[track].addEvent(time, event) def addVocalLyric(self, text): time = self.abs_time() assert time >= 0 if not self.useVocalTrack: return True track = [i for i,j in enumerate(self.song.parts) if self.partnumber == j][0] self.song.track[track].allWords[time] = text def addVocalStar(self, time): if time is None: time = self.abs_time() assert time >= 0 if not self.useVocalTrack: return True track = [i for i,j in enumerate(self.song.parts) if self.partnumber == j][0] self.song.track[track].starTimes.append(time) def addTempoEvent(self, event, time = None): #MFH - universal Tempo track handling if not isinstance(event, Tempo): return if time is None: time = self.abs_time() assert time >= 0 #add tempo events to the universal tempo track self.song.tempoEventTrack.addEvent(time, event) if self.logTempoEvents == 1: Log.debug("Tempo event added to Tempo track: " + str(time) + " - " + str(event.bpm) + "BPM" ) def addSpecialMidiEvent(self, track, event, time = None): #MFH if self.partnumber == -1: #Looks like notes have started appearing before any part information. Lets assume its part0 self.partnumber = self.song.parts[0] if (self.partnumber == None) and isinstance(event, MarkerNote): return True if time is None: time = self.abs_time() assert time >= 0 if track is None: for t in self.song.midiEventTracks: for s in t: s.addEvent(time, event) else: tracklist = [i for i,j in enumerate(self.song.parts) if self.partnumber == j] for i in tracklist: #Each track needs it's own copy of the event, otherwise they'll interfere eventcopy = copy.deepcopy(event) if track < len(self.song.midiEventTracks[i]): self.song.midiEventTracks[i][track].addEvent(time, eventcopy) def abs_time(self): def ticksToBeats(ticks, bpm): return (60000.0 * ticks) / (bpm * self.ticksPerBeat) if self.song.bpm: currentTime = midi.MidiOutStream.abs_time(self) scaledTime = 0.0 tempoMarkerTime = 0.0 currentBpm = self.song.bpm for i, marker in enumerate(self.tempoMarkers): time, bpm = marker if time > currentTime: break scaledTime += ticksToBeats(time - tempoMarkerTime, currentBpm) tempoMarkerTime, currentBpm = time, bpm return scaledTime + ticksToBeats(currentTime - tempoMarkerTime, currentBpm) return 0.0 def header(self, format, nTracks, division): self.ticksPerBeat = division if nTracks == 2: self.partTrack = 1 def tempo(self, value): bpm = 60.0 * 10.0**6 / value self.tempoMarkers.append((midi.MidiOutStream.abs_time(self), bpm)) if not self.song.bpm: self.song.setBpm(bpm) self.addEvent(None, Tempo(bpm)) self.addTempoEvent(Tempo(bpm)) #MFH def sequence_name(self, text): #if self.get_current_track() == 0: self.partnumber = None tempText = "Found sequence_name in MIDI: " + text + ", recognized as " tempText2 = "" if (text == "PART GUITAR" or text == "T1 GEMS" or text == "Click" or text == "MIDI out") and parts[GUITAR_PART] in self.song.parts: self.partnumber = parts[GUITAR_PART] if self.logSections == 1: tempText2 = "GUITAR_PART" elif text == "PART RHYTHM" and parts[RHYTHM_PART] in self.song.parts: self.partnumber = parts[RHYTHM_PART] if self.logSections == 1: tempText2 = "RHYTHM_PART" elif text == "PART BASS" and parts[BASS_PART] in self.song.parts: self.partnumber = parts[BASS_PART] if self.logSections == 1: tempText2 = "BASS_PART" elif text == "PART GUITAR COOP" and parts[LEAD_PART] in self.song.parts: self.partnumber = parts[LEAD_PART] if self.logSections == 1: tempText2 = "LEAD_PART" elif (text == "PART DRUM" or text == "PART DRUMS") and parts[DRUM_PART] in self.song.parts: self.partnumber = parts[DRUM_PART] if self.logSections == 1: tempText2 = "DRUM_PART" elif text == "PART VOCALS": # MFH self.partnumber = parts[VOCAL_PART] self.vocalTrack = True if self.partnumber in self.song.parts: self.useVocalTrack = True else: self.useVocalTrack = False else: self.vocalTrack = False #for rock band unaltered rip compatibility #-elif text == "PART DRUMS" and parts[DRUM_PART] in self.song.parts: #-self.partnumber = parts[DRUM_PART] #-elif self.get_current_track() <= 1 and parts [GUITAR_PART] in self.song.parts: #-#Oh dear, the track wasn't recognised, lets just assume it was the guitar part #-self.partnumber = parts[GUITAR_PART] if self.logSections == 1: Log.debug(tempText + tempText2) self.guitarSoloIndex = 0 self.guitarSoloActive = False def note_on(self, channel, note, velocity): if self.partnumber == None: return self.velocity[note] = velocity self.heldNotes[(self.get_current_track(), channel, note)] = self.abs_time() def note_off(self, channel, note, velocity): if self.partnumber == None: return try: startTime = self.heldNotes[(self.get_current_track(), channel, note)] endTime = self.abs_time() del self.heldNotes[(self.get_current_track(), channel, note)] if self.vocalTrack: if note > 39 and note < 84: self.addVocalEvent(VocalNote(note, endTime - startTime), time = startTime) elif note == 96: #tambourine self.addVocalEvent(VocalNote(note, 1, True), time = startTime) elif note == 105 or note == 106: if startTime not in self.vocalOverlapCheck: self.addVocalEvent(VocalPhrase(endTime - startTime), time = startTime) self.vocalOverlapCheck.append(startTime) elif note == overDriveMarkingNote: self.addVocalStar(startTime) return if note in noteMap: track, number = noteMap[note] self.addEvent(track, Note(number, endTime - startTime, special = self.velocity[note] == 127), time = startTime) #MFH: use self.midiEventTracks to store all the special MIDI marker notes, keep the clutter out of the main notes lists # also -- to make use of marker notes in real-time, must add a new attribute to MarkerNote class "endMarker" # if this is == True, then the note is just an event to mark the end of the previous note (which has length and is used in other ways) elif note == overDriveMarkingNote: #MFH self.song.hasStarpowerPaths = True self.earlyHitWindowSize = EARLY_HIT_WINDOW_NONE #for diff in self.song.difficulty: for diff in difficulties: #self.addEvent(diff, MarkerNote(note, endTime - startTime), time = startTime) self.addSpecialMidiEvent(diff, MarkerNote(note, endTime - startTime), time = startTime) self.addSpecialMidiEvent(diff, MarkerNote(note, 1, endMarker = True), time = endTime+1) #ending marker note if self.logMarkerNotes == 1: Log.debug("RB Overdrive MarkerNote at %f added to part: %s and difficulty: %s" % ( startTime, self.partnumber, difficulties[diff] ) ) elif note == starPowerMarkingNote: #MFH self.song.hasStarpowerPaths = True #for diff in self.song.difficulty: for diff in difficulties: #self.addEvent(diff, MarkerNote(note, endTime - startTime), time = startTime) self.addSpecialMidiEvent(diff, MarkerNote(note, endTime - startTime), time = startTime) self.addSpecialMidiEvent(diff, MarkerNote(note, 1, endMarker = True), time = endTime+1) #ending marker note if self.logMarkerNotes == 1: Log.debug("GH Starpower (or RB Solo) MarkerNote at %f added to part: %s and difficulty: %s" % ( startTime, self.partnumber, difficulties[diff] ) ) elif note == freestyleMarkingNote: #MFH - for drum fills and big rock endings self.song.hasFreestyleMarkings = True #for diff in self.song.difficulty: for diff in difficulties: #self.addEvent(diff, MarkerNote(note, endTime - startTime), time = startTime) self.addSpecialMidiEvent(diff, MarkerNote(note, endTime - startTime), time = startTime) #self.addSpecialMidiEvent(diff, MarkerNote(note, 1, endMarker = True), time = endTime+1) #ending marker note if self.logMarkerNotes == 1: Log.debug("RB freestyle section (drum fill or BRE) at %f added to part: %s and difficulty: %s" % ( startTime, self.partnumber, difficulties[diff] ) ) else: #Log.warn("MIDI note 0x%x at %d does not map to any game note." % (note, self.abs_time())) pass except KeyError: Log.warn("MIDI note 0x%x on channel %d ending at %d was never started." % (note, channel, self.abs_time())) #MFH - OK - this needs to be optimized. #There should be a separate "Sections" track, and a separate "Lyrics" track created for their respective events #Then another separate "Text Events" track to put all the unused events, so they don't bog down the game when it looks through / filters / sorts these track event lists #myfingershurt: adding MIDI text event access #these events happen on their own track, and are processed after the note tracks. #just mark the guitar solo sections ahead of time #and then write an iteration routine to go through whatever track / difficulty is being played in GuitarScene #to find these markers and count the notes and add a new text event containing each solo's note count def text(self, text): if text.find("GNMIDI") < 0: #to filter out the midi class illegal usage / trial timeout messages #Log.debug(str(self.abs_time()) + "-MIDI Text: " + text) if self.readTextAndLyricEvents > 0: #MFH - if sequence name is PART VOCALS then look for text event lyrics if self.vocalTrack: if text.find("[") < 0: #not a marker event = TextEvent(text, 400.0) self.addVocalLyric(text) self.song.hasMidiLyrics = True self.song.eventTracks[TK_LYRICS].addEvent(self.abs_time(), event) #MFH - add an event to the lyrics track else: unusedEvent = None event = None gSoloEvent = False #also convert all underscores to spaces so it look better text = text.replace("_"," ") if text.lower().find("section") >= 0: self.guitarSoloSectionMarkers = True #GH1 dont use section markers... GH2+ do #strip unnecessary text / chars: text = text.replace("section","") text = text.replace("[","") text = text.replace("]","") #also convert "gtr" to "Guitar" text = text.replace("gtr","Guitar") #event = TextEvent("SEC: " + text, 250.0) event = TextEvent(text, 250.0) if text.lower().find("big rock ending") >= 0: curTime = self.abs_time() Log.debug("Big Rock Ending section event marker found at " + str(curTime) ) self.song.breMarkerTime = curTime if text.lower().find("solo") >= 0 and text.lower().find("drum") < 0 and text.lower().find("outro") < 0 and text.lower().find("organ") < 0 and text.lower().find("synth") < 0 and text.lower().find("bass") < 0 and text.lower().find("harmonica") < 0: gSoloEvent = True gSolo = True elif text.lower().find("guitar") >= 0 and text.lower().find("lead") >= 0: #Foreplay Long Time "[section_gtr_lead_1]" gSoloEvent = True gSolo = True elif text.lower().find("guitar") >= 0 and text.lower().find("line") >= 0: #support for REM Orange Crush style solos gSoloEvent = True gSolo = True elif text.lower().find("guitar") >= 0 and text.lower().find("ostinato") >= 0: #support for Pleasure solos "[section gtr_ostinato]" gSoloEvent = True gSolo = True else: #this is the cue to end solos... gSoloEvent = True gSolo = False elif text.lower().find("solo") >= 0 and text.find("[") < 0 and text.lower().find("drum") < 0 and text.lower().find("map") < 0 and text.lower().find("play") < 0 and not self.guitarSoloSectionMarkers: #event = TextEvent("SEC: " + text, 250.0) event = TextEvent(text, 250.0) gSoloEvent = True if text.lower().find("off") >= 0: gSolo = False else: gSolo = True elif (text.lower().find("verse") >= 0 or text.lower().find("chorus") >= 0) and text.find("[") < 0 and not self.guitarSoloSectionMarkers: #this is an alternate GH1-style solo end marker #event = TextEvent("SEC: " + text, 250.0) event = TextEvent(text, 250.0) gSoloEvent = True gSolo = False elif text.lower().find("gtr") >= 0 and text.lower().find("off") >= 0 and text.find("[") < 0 and not self.guitarSoloSectionMarkers: #this is an alternate GH1-style solo end marker #also convert "gtr" to "Guitar" text = text.replace("gtr","Guitar") #event = TextEvent("SEC: " + text, 100.0) event = TextEvent(text, 100.0) gSoloEvent = True gSolo = False else: #unused text event #unusedEvent = TextEvent("TXT: " + text, 100.0) unusedEvent = TextEvent(text, 100.0) #now, check for guitar solo status change: soloSlop = 150.0 if gSoloEvent: if gSolo: if not self.guitarSoloActive: self.guitarSoloActive = True soloEvent = TextEvent("GSOLO ON", 250.0) Log.debug("GSOLO ON event " + event.text + " found at time " + str(self.abs_time()) ) self.song.eventTracks[TK_GUITAR_SOLOS].addEvent(self.abs_time(), soloEvent) #MFH - add an event to the guitar solos track else: #this is the cue to end solos... if self.guitarSoloActive: #MFH - here, check to make sure we're not ending a guitar solo that has just started!! curTime = self.abs_time() if self.song.eventTracks[TK_GUITAR_SOLOS][-1][0] < curTime: self.guitarSoloActive = False soloEvent = TextEvent("GSOLO OFF", 250.0) Log.debug("GSOLO OFF event " + event.text + " found at time " + str(curTime) ) self.guitarSoloIndex += 1 self.song.eventTracks[TK_GUITAR_SOLOS].addEvent(curTime, soloEvent) #MFH - add an event to the guitar solos track if event: curTime = self.abs_time() if len(self.song.eventTracks[TK_SECTIONS]) <= 1: self.song.eventTracks[TK_SECTIONS].addEvent(curTime, event) #MFH - add an event to the sections track elif len(self.song.eventTracks[TK_SECTIONS]) > 1: #ensure it exists first if self.song.eventTracks[TK_SECTIONS][-1][0] < curTime: #ensure we're not adding two consecutive sections to the same location! self.song.eventTracks[TK_SECTIONS].addEvent(curTime, event) #MFH - add an event to the sections track elif unusedEvent: self.song.eventTracks[TK_UNUSED_TEXT].addEvent(self.abs_time(), unusedEvent) #MFH - add an event to the unused text track #for track in self.song.tracks: # for t in track: # t.addEvent(self.abs_time(), event) #myfingershurt: adding MIDI lyric event access def lyric(self, text): if text.find("GNMIDI") < 0: #to filter out the midi class illegal usage / trial timeout messages #Log.debug(str(self.abs_time()) + "-MIDI Lyric: " + text) if self.readTextAndLyricEvents > 0: #event = TextEvent("LYR: " + text, 400.0) event = TextEvent(text, 400.0) self.addVocalLyric(text) self.song.hasMidiLyrics = True self.song.eventTracks[TK_LYRICS].addEvent(self.abs_time(), event) #MFH - add an event to the lyrics track #for track in self.song.tracks: # for t in track: # t.addEvent(self.abs_time(), event) class MidiSectionReader(midi.MidiOutStream): # We exit via this exception so that we don't need to read the whole file in class Done: pass def __init__(self): midi.MidiOutStream.__init__(self) self.difficulties = [] self.logClassInits = Config.get("game", "log_class_inits") if self.logClassInits == 1: Log.debug("MidiSectionReader class init (song.py)...") self.logSections = Config.get("game", "log_sections") #MFH: practice section support: self.ticksPerBeat = 480 self.sections = [] self.tempoMarkers = [] self.guitarSoloSectionMarkers = False self.bpm = None self.noteCountSections = [] self.nextSectionMinute = 0.25 def header(self, format=0, nTracks=1, division=96): self.ticksPerBeat = division def abs_time(self): def ticksToBeats(ticks, bpm): return (60000.0 * ticks) / (bpm * self.ticksPerBeat) if self.bpm: currentTime = midi.MidiOutStream.abs_time(self) scaledTime = 0.0 tempoMarkerTime = 0.0 currentBpm = self.bpm for i, marker in enumerate(self.tempoMarkers): time, bpm = marker if time > currentTime: break scaledTime += ticksToBeats(time - tempoMarkerTime, currentBpm) tempoMarkerTime, currentBpm = time, bpm return scaledTime + ticksToBeats(currentTime - tempoMarkerTime, currentBpm) return 0.0 def tempo(self, value): self.bpm = 60.0 * 10.0**6 / value self.tempoMarkers.append((midi.MidiOutStream.abs_time(self), self.bpm)) def note_on(self, channel, note, velocity): pos = float(midi.MidiOutStream.abs_time(self)) if (pos / 60000) >= self.nextSectionMinute: text = "%d:%02d -> " % (pos / 60000, (pos % 60000) / 1000) + "Section " + str(round(self.nextSectionMinute,2)) self.nextSectionMinute += 0.25 #MFH - only log if enabled if self.logSections == 1: Log.debug("Found potential default practice section: " + str(pos) + " - " + text) self.noteCountSections.append([text,pos]) def lyric(self, text): #filter lyric events if text.find("GNMIDI") < 0: #to filter out the midi class illegal usage / trial timeout messages #Log.debug(str(self.abs_time()) + "-MIDI Lyric: " + text) text = "" #MFH - adding text event / section retrieval here #also must prevent "Done" flag setting so can read whole MIDI file, all text events def text(self, text): if text.find("GNMIDI") < 0: #to filter out the midi class illegal usage / trial timeout messages pos = self.abs_time() #Log.debug("Found MidiInfoReader text event: " + text) #if self.readTextAndLyricEvents > 0: text = text.replace("_"," ") if text.lower().find("section") >= 0: if not self.guitarSoloSectionMarkers: self.guitarSoloSectionMarkers = True #GH1 dont use section markers... GH2+ do self.sections = [] #reset sections #strip unnecessary text / chars: text = text.replace("section","") text = text.replace("[","") text = text.replace("]","") #also convert "gtr" to "Guitar" text = text.replace("gtr","Guitar") #MFH - only log if enabled if self.logSections == 1: Log.debug("Found
potential RB-style practice section: " + str(pos) + " - " + text) text = "%d:%02d -> " % (pos / 60000, (pos % 60000) / 1000) + text self.sections.append([text,pos]) elif text.lower().find("solo") >= 0 and text.find("[") < 0 and text.lower().find("drum") < 0 and text.lower().find("map") < 0 and text.lower().find("play") < 0 and not self.guitarSoloSectionMarkers: #MFH - only log if enabled if self.logSections == 1: Log.debug("Found potential GH1-style practice section: " + str(pos) + " - " + text) text = "%d:%02d -> " % (pos / 60000, (pos % 60000) / 1000) + text self.sections.append([text,pos]) elif text.lower().find("verse") >= 0 and text.find("[") < 0 and not self.guitarSoloSectionMarkers: #this is an alternate GH1-style solo end marker #MFH - only log if enabled if self.logSections == 1: Log.debug("Found potential GH1-style practice section: " + str(pos) + " - " + text) text = "%d:%02d -> " % (pos / 60000, (pos % 60000) / 1000) + text self.sections.append([text,pos]) elif text.lower().find("gtr") >= 0 and text.lower().find("off") >= 0 and text.find("[") < 0 and not self.guitarSoloSectionMarkers: #this is an alternate GH1-style solo end marker #also convert "gtr" to "Guitar" text = text.replace("gtr","Guitar") #MFH - only log if enabled if self.logSections == 1: Log.debug("Found potential GH1-style practice section: " + str(pos) + " - " + text) text = "%d:%02d -> " % (pos / 60000, (pos % 60000) / 1000) + text self.sections.append([text,pos]) elif not self.guitarSoloSectionMarkers: #General GH1-style sections. Messy but effective. #MFH - only log if enabled if self.logSections == 1: Log.debug("Found potential GH1-style practice section: " + str(pos) + " - " + text) text = "%d:%02d -> " % (pos / 60000, (pos % 60000) / 1000) + text self.sections.append([text,pos]) else: #unused text event text = "" class SongQueue: def __init__(self): self.songName = [] self.library = [] self.players = [] self.difficulty1 = [] self.difficulty2 = [] self.part1 = [] self.part2 = [] self.logClassInits = Config.get("game", "log_class_inits") if self.logClassInits == 1: Log.debug("SongQueue class init (song.py)...") def addSong(self, songName, library, players, difficulty1, difficulty2, part1, part2): self.songName.append(songName) self.library.append(library) self.players.append(players) self.difficulty1.append(difficulty1) self.difficulty2.append(difficulty2) self.part1.append(part1) self.part2.append(part2) def nextSong(self, counter): #should be called getSong now, but don't know how to refactor in python nextSong = [self.songName[counter], self.library[counter], self.players[counter], self.difficulty1[counter], self.difficulty2[counter], self.part1[counter], self.part2[counter]] return nextSong def load(self, engine, loadfile): path = os.path.abspath(engine.resource.loadfile("queues", loadame)) self = pickle.load("%s%s" %path %loadfile) def save(self, engine, fileName): if not fileName: return False path = os.path.abspath(engine.resource.fileName("queues", fileName)) if os.path.isdir(path): return False savefile = open(("%(p)s%(f)s.que") % {"p" : path, "f": fileName}, "w") pickle.dump(self, savefile) savefile.close return True def reset(self): self.songName = [] self.library = [] self.players = [] self.difficulty1 = [] self.difficulty2 = [] self.part1 = [] self.part2 = [] class MidiPartsDiffReader(midi.MidiOutStream): def __init__(self, forceGuitar = False): midi.MidiOutStream.__init__(self) self.parts = [] self.difficulties = {} self.currentPart = -1 self.nextPart = 0 self.forceGuitar = forceGuitar self.firstTrack = False self.notesFound = [0, 0, 0, 0] self._drumFound = False self._ODNoteFound = False self._SPNoteFound = False self.logClassInits = Config.get("game", "log_class_inits") if self.logClassInits == 1: Log.debug("MidiPartsDiffReader class init (song.py)...") self.logSections = Config.get("game", "log_sections") def getMidiStyle(self): if self._ODNoteFound: Log.debug("MIDI Type identified as RB") return MIDI_TYPE_RB elif self._drumFound and self._SPNoteFound: Log.debug("MIDI Type identified as WT") return MIDI_TYPE_WT else: Log.debug("MIDI Type identified as GH") return MIDI_TYPE_GH midiStyle = property(getMidiStyle) def start_of_track(self, n_track=0): if self.forceGuitar: if not self.firstTrack: if not parts[GUITAR_PART] in self.parts: part = parts[GUITAR_PART] self.parts.append(part) self.nextPart = None self.currentPart = part.id self.notesFound = [0, 0, 0, 0] self.difficulties[part.id] = [] if self.logSections == 1: tempText = "No recognized tracks found. Using first track, and identifying it as " tempText2 = "GUITAR_PART" Log.debug(tempText + tempText2) self.firstTrack = True else: Log.notice("This song has multiple tracks, none properly named. Behavior may be erratic.") def sequence_name(self, text): if self.logSections == 1: tempText = "MIDI sequence_name found: " + text + ", recognized and added to list as " tempText2 = "" if text == "PART GUITAR" or text == "T1 GEMS" or text == "Click": if not parts[GUITAR_PART] in self.parts: self.nextPart = parts[GUITAR_PART] self.currentPart = self.nextPart.id self.notesFound = [0, 0, 0, 0] if self.logSections == 1: tempText2 = "GUITAR_PART" Log.debug(tempText + tempText2) elif text == "PART RHYTHM": if not parts[RHYTHM_PART] in self.parts: self.nextPart = parts[RHYTHM_PART] self.currentPart = self.nextPart.id self.notesFound = [0, 0, 0, 0] if self.logSections == 1: tempText2 = "RHYTHM_PART" Log.debug(tempText + tempText2) elif text == "PART BASS": if not parts[BASS_PART] in self.parts: self.nextPart = parts[BASS_PART] self.currentPart = self.nextPart.id self.notesFound = [0, 0, 0, 0] if self.logSections == 1: tempText2 = "BASS_PART" Log.debug(tempText + tempText2) elif text == "PART GUITAR COOP": if not parts[LEAD_PART] in self.parts: self.nextPart = parts[LEAD_PART] self.currentPart = self.nextPart.id self.notesFound = [0, 0, 0, 0] if self.logSections == 1: tempText2 = "LEAD_PART" Log.debug(tempText + tempText2) #myfingershurt: drums, rock band rip compatible :) elif text == "PART DRUM" or text == "PART DRUMS": if not parts[DRUM_PART] in self.parts: self.nextPart = parts[DRUM_PART] self.currentPart = self.nextPart.id self.notesFound = [0, 0, 0, 0] self._drumFound = True #drum parts appear to be obligatory in WT songs, even if phantom. if self.logSections == 1: tempText2 = "DRUM_PART" Log.debug(tempText + tempText2) elif text == "PART VOCALS": if not parts[VOCAL_PART] in self.parts: part = parts[VOCAL_PART] self.parts.append(part) self.nextPart = None self.currentPart = part.id self.difficulties[part.id] = difficulties.values() if self.logSections == 1: tempText2 = "VOCAL_PART" Log.debug(tempText + tempText2) else: self.currentPart = -1 def addPart(self): self.parts.append(self.nextPart) self.difficulties[self.nextPart.id] = [] self.nextPart = None def note_on(self, channel, note, velocity): if self.currentPart == -1: return if note == overDriveMarkingNote: self._ODNoteFound = True elif note == starPowerMarkingNote: self._SPNoteFound = True try: try: if len(self.difficulties[self.currentPart]) == len(difficulties): return except KeyError: pass track, number = noteMap[note] self.notesFound[track] += 1 if self.notesFound[track] > 5: if self.nextPart is not None: self.addPart() diff = difficulties[track] if not diff in self.difficulties[self.currentPart]: self.difficulties[self.currentPart].append(diff) except KeyError: pass def loadSong(engine, name, library = DEFAULT_LIBRARY, seekable = False, playbackOnly = False, notesOnly = False, part = [parts[GUITAR_PART]], practiceMode = False, practiceSpeed = 1.0): Log.debug("loadSong function call (song.py)...") crowdsEnabled = engine.config.get("audio", "crowd_tracks_enabled") #RF-mod (not needed?) guitarFile = engine.resource.fileName(library, name, "guitar.ogg") songFile = engine.resource.fileName(library, name, "song.ogg") rhythmFile = engine.resource.fileName(library, name, "rhythm.ogg") crowdFile = engine.resource.fileName(library, name, "crowd.ogg") logUneditedMidis = engine.config.get("log", "log_unedited_midis") useUneditedMidis = engine.config.get("debug", "use_unedited_midis") if useUneditedMidis == 1: #auto noteFile = engine.resource.fileName(library, name, "notes-unedited.mid", writable = True) #if os.path.exists(os.path.join(os.path.dirname(self.fileName), "notes-unedited.mid")) if os.path.isfile(noteFile): if logUneditedMidis == 1: Log.debug("notes-unedited.mid found, using instead of notes.mid! - " + name) else: noteFile = engine.resource.fileName(library, name, "notes.mid", writable = True) if logUneditedMidis == 1: Log.debug("notes-unedited.mid not found, using notes.mid - " + name) else: noteFile = engine.resource.fileName(library, name, "notes.mid", writable = True) if logUneditedMidis == 1: Log.debug("notes-unedited.mid not found, using notes.mid - " + name) #noteFile = engine.resource.fileName(library, name, "notes.mid", writable = True) infoFile = engine.resource.fileName(library, name, "song.ini", writable = True) scriptFile = engine.resource.fileName(library, name, "script.txt") previewFile = engine.resource.fileName(library, name, "preview.ogg") #myfingershurt: drumFile = engine.resource.fileName(library, name, "drums.ogg") if seekable: if os.path.isfile(guitarFile) and os.path.isfile(songFile): # TODO: perform mixing here songFile = guitarFile guitarFile = None rhythmFile = "" else: songFile = guitarFile guitarFile = None rhythmFile = "" if not os.path.isfile(songFile): songFile = guitarFile guitarFile = None if not os.path.isfile(rhythmFile): rhythmFile = None #if os.path.isfile(alternteRhythmFile): #support for bass.ogg #rhythmFile = alternateRhythmFile if not os.path.isfile(drumFile): drumFile = None if not os.path.isfile(crowdFile): crowdFile = None #if os.path.isfile(songCrowdFile) and useSongCrowdTrack: #supports song+crowd... may be (re)implemented later. #crowdFile = songCrowdFile #Log.warn("crowd.ogg file not found. Using song+crowd.ogg instead! Volumes may be off.") if crowdsEnabled == 0: crowdFile = None if songFile != None and guitarFile != None: #check for the same file songStat = os.stat(songFile) guitarStat = os.stat(guitarFile) #Simply checking file size, no md5 if songStat.st_size == guitarStat.st_size: guitarFile = None if practiceMode: #single track practice mode only! if part[0] == parts[GUITAR_PART] and guitarFile != None: songFile = guitarFile elif part[0] == parts[BASS_PART] and rhythmFile != None: songFile = rhythmFile elif part[0] == parts[DRUM_PART] and drumFile != None: songFile = drumFile guitarFile = None rhythmFile = None drumFile = None crowdFile = None slowDownFactor = practiceSpeed else: slowDownFactor = engine.config.get("audio", "speed_factor") engine.setSpeedFactor(slowDownFactor) #MFH #MFH - check for slowdown mode here. If slowdown, and single track song (or practice mode), # duplicate single track to a streamingAudio track so the slowed down version can be heard. if engine.audioSpeedFactor != 1: crowdFile = None #count tracks: audioTrackCount = 0 if guitarFile: audioTrackCount += 1 if rhythmFile: audioTrackCount += 1 if drumFile: audioTrackCount += 1 if audioTrackCount < 1: if part[0] == parts[GUITAR_PART]: guitarFile = songFile elif part[0] == parts[BASS_PART]: rhythmFile = songFile elif part[0] == parts[DRUM_PART]: drumFile = songFile else: guitarFile = songFile if playbackOnly: noteFile = None crowdFile = None if os.path.isfile(previewFile): songFile = previewFile guitarFile = None rhythmFile = None drumFile = None if notesOnly: songFile = None crowdFile = None guitarFile = None rhythmFile = None previewFile = None drumFile = None song = Song(engine, infoFile, songFile, guitarFile, rhythmFile, noteFile, scriptFile, part, drumFile, crowdFile) return song def loadSongInfo(engine, name, library = DEFAULT_LIBRARY): #RF-mod (not needed?) infoFile = engine.resource.fileName(library, name, "song.ini", writable = True) return SongInfo(infoFile, library) def createSong(engine, name, guitarTrackName, backgroundTrackName, rhythmTrackName = None, library = DEFAULT_LIBRARY): #RF-mod (not needed?) path = os.path.abspath(engine.resource.fileName(library, name, writable = True)) os.makedirs(path) guitarFile = engine.resource.fileName(library, name, "guitar.ogg", writable = True) songFile = engine.resource.fileName(library, name, "song.ogg", writable = True) noteFile = engine.resource.fileName(library, name, "notes.mid", writable = True) infoFile = engine.resource.fileName(library, name, "song.ini", writable = True) shutil.copy(guitarTrackName, guitarFile) if backgroundTrackName: shutil.copy(backgroundTrackName, songFile) else: songFile = guitarFile guitarFile = None if rhythmTrackName: rhythmFile = engine.resource.fileName(library, name, "rhythm.ogg", writable = True) shutil.copy(rhythmTrackName, rhythmFile) else: rhythmFile = None f = open(noteFile, "wb") m = midi.MidiOutFile(f) m.header() m.start_of_track() m.update_time(0) m.end_of_track() m.eof() m.write() f.close() song = Song(engine, infoFile, songFile, guitarFile, rhythmFile, noteFile) song.info.name = name song.save() return song def getDefaultLibrary(engine): return LibraryInfo(DEFAULT_LIBRARY, engine.resource.fileName(DEFAULT_LIBRARY, "library.ini")) def getAvailableLibraries(engine, library = DEFAULT_LIBRARY): Log.debug("Song.getAvailableLibraries function call...library = " + str(library) ) # Search for libraries in both the read-write and read-only directories songRoots = [engine.resource.fileName(library), engine.resource.fileName(library, writable = True)] libraries = [] libraryRoots = [] for songRoot in set(songRoots): if (os.path.exists(songRoot) == False): return libraries for libraryRoot in os.listdir(songRoot): if libraryRoot == ".svn": #MFH - filter out the hidden SVN control folder! continue libraryRoot = os.path.join(songRoot, libraryRoot) if not os.path.isdir(libraryRoot): continue if os.path.isfile(os.path.join(libraryRoot, "notes.mid")): continue if os.path.isfile(os.path.join(libraryRoot, "notes-unedited.mid")): continue libName = library + os.path.join(libraryRoot.replace(songRoot, "")) libraries.append(LibraryInfo(libName, os.path.join(libraryRoot, "library.ini"))) #Log.debug("Library added: " + str(libraries) ) continue # why were these here? we must filter out empty libraries - coolguy567 #MFH - I'll tell you why -- so that empty ("tiered" / organizational) folders are displayed, granting access to songs in subfolders... dirs = os.listdir(libraryRoot) for name in dirs: if os.path.isfile(os.path.join(libraryRoot, name, "song.ini")): if not libraryRoot in libraryRoots: libName = library + os.path.join(libraryRoot.replace(songRoot, "")) libraries.append(LibraryInfo(libName, os.path.join(libraryRoot, "library.ini"))) #Log.debug("Library added: " + str(libraries) ) libraryRoots.append(libraryRoot) libraries.sort(lambda a, b: cmp(a.name.lower(), b.name.lower())) return libraries def getAvailableSongs(engine, library = DEFAULT_LIBRARY, includeTutorials = False, progressCallback = lambda p: None): order = engine.config.get("game", "sort_order") tut = engine.config.get("game", "tut") direction = engine.config.get("game", "sort_direction") if tut: includeTutorials = True #MFH - Career Mode determination: gameMode1p = engine.config.get("game","game_mode") if gameMode1p == 2: careerMode = True else: careerMode = False # Search for songs in both the read-write and read-only directories if library == None: return [] songRoots = [engine.resource.fileName(library), engine.resource.fileName(library, writable = True)] names = [] for songRoot in songRoots: if (os.path.exists(songRoot) == False): return [] for name in os.listdir(songRoot): #if not os.path.isfile(os.path.join(songRoot, name, "notes.mid")): if ( not os.path.isfile(os.path.join(songRoot, name, "notes.mid")) ) and ( not os.path.isfile(os.path.join(songRoot, name, "notes-unedited.mid")) ): continue if not os.path.isfile(os.path.join(songRoot, name, "song.ini")) or name.startswith("."): #if not os.path.isfile(os.path.join(songRoot, name, "song.ini")): continue if not name in names: names.append(name) songs = [] for name in names: progressCallback(len(songs)/float(len(names))) songs.append(SongInfo(engine.resource.fileName(library, name, "song.ini", writable = True), library, allowCacheUsage=True)) if len(songs) and canCache and Config.get("performance", "cache_song_metadata"): cache = cacheManager.getCache(songs[0].fileName) if cache != None: #stump: clean up the cache if cache.execute('DELETE FROM `songinfo` WHERE `hash` NOT IN (' + ','.join("'%s'" % s.stateHash for s in songs) + ')').rowcount > 0: cache.execute('VACUUM') # compact the database if anything was deleted on the previous line cache.commit() # commit any changes to the cache all at once if not includeTutorials: songs = [song for song in songs if not song.tutorial] songs = [song for song in songs if not song.artist == '=FOLDER='] #coolguy567's unlock system if careerMode: for song in songs: if song.getUnlockRequire() != "": song.setLocked(False) for song2 in songs: if song.getUnlockRequire() == song2.getUnlockID() and not song2.completed: song.setLocked(True) else: song.setLocked(False) instrument = engine.config.get("game", "songlist_instrument") theInstrumentDiff = instrumentDiff[instrument] if direction == 0: if order == 1: songs.sort(lambda a, b: cmp(a.artist.lower(), b.artist.lower())) elif order == 2: songs.sort(lambda a, b: cmp(int(b.count+str(0)), int(a.count+str(0)))) elif order == 0: songs.sort(lambda a, b: cmp(a.name.lower(), b.name.lower())) elif order == 3: songs.sort(lambda a, b: cmp(a.album.lower(), b.album.lower())) elif order == 4: songs.sort(lambda a, b: cmp(a.genre.lower(), b.genre.lower())) elif order == 5: songs.sort(lambda a, b: cmp(a.year.lower(), b.year.lower())) elif order == 6: songs.sort(lambda a, b: cmp(a.diffSong, b.diffSong)) elif order == 7: songs.sort(lambda a, b: cmp(theInstrumentDiff(a), theInstrumentDiff(b))) elif order == 8: songs.sort(lambda a, b: cmp(a.icon.lower(), b.icon.lower())) else: if order == 1: songs.sort(lambda a, b: cmp(b.artist.lower(), a.artist.lower())) elif order == 2: songs.sort(lambda a, b: cmp(int(a.count+str(0)), int(b.count+str(0)))) elif order == 0: songs.sort(lambda a, b: cmp(b.name.lower(), a.name.lower())) elif order == 3: songs.sort(lambda a, b: cmp(b.album.lower(), a.album.lower())) elif order == 4: songs.sort(lambda a, b: cmp(b.genre.lower(), a.genre.lower())) elif order == 5: songs.sort(lambda a, b: cmp(b.year.lower(), a.year.lower())) elif order == 6: songs.sort(lambda a, b: cmp(b.diffSong, a.diffSong)) elif order == 7: songs.sort(lambda a, b: cmp(theInstrumentDiff(b), theInstrumentDiff(a))) elif order == 8: songs.sort(lambda a, b: cmp(b.icon.lower(), a.icon.lower())) return songs #coolguy567's unlock system def getSortingTitles(engine, songList = []): sortOrder = engine.config.get("game","sort_order") titles = [] sortTitles = [] instrument = engine.config.get("game", "songlist_instrument") theInstrumentDiff = instrumentDiff[instrument] #if sortOrder == 0: # for i in range (65,90): # sortTitles.append(SortTitleInfo(chr(i))) #elif sortOrder == 1: for songItem in songList: if sortOrder == 1: try: titles.index(songItem.artist.lower()) except ValueError: titles.append(songItem.artist.lower()) sortTitles.append(SortTitleInfo(songItem.artist)) elif sortOrder == 0: try: if songItem.name[0].isdigit(): sortName = "123" elif not songItem.name[0].isalnum(): sortName = "!@#" else: sortName = songItem.name[0].lower() titles.index(sortName) except ValueError: if songItem.name[0].isdigit(): sortName = "123" elif not songItem.name[0].isalnum(): sortName = "!@#" else: sortName = songItem.name[0].lower() titles.append(sortName) sortTitles.append(SortTitleInfo(sortName.upper())) elif sortOrder == 2: try: titles.index(songItem.count) except ValueError: titles.append(songItem.count) if songItem.count == "": sortTitles.append(SortTitleInfo("0")) titles.append("0") else: sortTitles.append(SortTitleInfo(songItem.count)) elif sortOrder == 3: try: titles.index(songItem.album.lower()) except ValueError: titles.append(songItem.album.lower()) sortTitles.append(SortTitleInfo(songItem.album)) elif sortOrder == 4: try: titles.index(songItem.genre.lower()) except ValueError: titles.append(songItem.genre.lower()) sortTitles.append(SortTitleInfo(songItem.genre)) elif sortOrder == 5: try: titles.index(songItem.year) except ValueError: titles.append(songItem.year) sortTitles.append(SortTitleInfo(songItem.year)) elif sortOrder == 6: try: titles.index(songItem.diffSong) except ValueError: titles.append(songItem.diffSong) sortTitles.append(SortTitleInfo(str(songItem.diffSong))) elif sortOrder == 7: try: titles.index(theInstrumentDiff(songItem)) except ValueError: titles.append(theInstrumentDiff(songItem)) sortTitles.append(SortTitleInfo(str(theInstrumentDiff(songItem)))) elif sortOrder == 8: try: titles.index(songItem.icon.lower()) except ValueError: titles.append(songItem.icon.lower()) sortTitles.append(SortTitleInfo(songItem.icon)) return sortTitles def getAvailableTitles(engine, library = DEFAULT_LIBRARY): gameMode1p = engine.config.get("game","game_mode") if library == None: return [] path = os.path.join(engine.resource.fileName(library, writable = True), "titles.ini") if not os.path.isfile(path): return [] config = Config.MyConfigParser() titles = [] try: config.read(path) sections = config.get("titles", "sections") for section in sections.split(): titles.append(TitleInfo(config, section)) if gameMode1p == 2: titles.append(BlankSpaceInfo(_("End of Career"))) except: Log.debug("titles.ini could not be read (song.py)") return [] return titles def getAvailableSongsAndTitles(engine, library = DEFAULT_LIBRARY, includeTutorials = False, progressCallback = lambda p: None): listingMode = engine.config.get("game","song_listing_mode") if library == None: return [] #MFH - Career Mode determination: gameMode1p = engine.config.get("game","game_mode") if gameMode1p == 2: careerMode = True quickPlayMode = False else: careerMode = False if gameMode1p == 0: quickPlayMode = True else: quickPlayMode = False quickPlayCareerTiers = engine.config.get("game", "quickplay_tiers") if listingMode == 0 or careerMode: items = getAvailableSongs(engine, library, includeTutorials, progressCallback=progressCallback) if quickPlayMode and quickPlayCareerTiers == 0: titles = [] else: if quickPlayCareerTiers == 1 or careerMode: titles = getAvailableTitles(engine, library) else: titles = getSortingTitles(engine, items) items = items + titles else: items = [] titles = [] rootDir = engine.resource.fileName(DEFAULT_LIBRARY) items = getAvailableSongs(engine, rootDir, includeTutorials, progressCallback=progressCallback) for root, subFolders, files in os.walk(rootDir): for folder in subFolders: libName = os.path.join(root,folder) items = items + getAvailableSongs(engine, libName, includeTutorials, progressCallback=progressCallback) if quickPlayMode and quickPlayCareerTiers == 0: titles = [] else: if quickPlayCareerTiers == 1: titles = titles + getAvailableTitles(engine, libName) if quickPlayCareerTiers == 2: titles = getSortingTitles(engine, items) items = items + titles items.sort(lambda a, b: compareSongsAndTitles(engine, a, b)) if quickPlayMode and len(items) != 0: items.insert(0, RandomSongInfo()) if careerMode and titles != []: items.append(BlankSpaceInfo()) items.append(CareerResetterInfo()) return items def compareSongsAndTitles(engine, a, b): #MFH - want to push all non-career songs in a folder below all titles and career songs #When an unlock_id does not exist in song.ini, a blank string "" value is returned. #Can check for this to determine that this song should be below any titles or non-None unlock_id's #>>> def numeric_compare(x, y): #>>> if x>y: #>>> return 1 #>>> elif x==y: #>>> return 0 #>>> else: # x>> return -1 #MFH - Career Mode determination: gameMode1p = engine.config.get("game","game_mode") order = engine.config.get("game", "sort_order") #MFH - must check here for an invalid Sort Order setting and correct it! orderings = engine.config.getOptions("game", "sort_order")[1] if order >= len(orderings): order = 0 engine.config.set("game", "sort_order", order) instrument = engine.config.get("game", "songlist_instrument") theInstrumentDiff = instrumentDiff[instrument] direction = engine.config.get("game", "sort_direction") if gameMode1p == 2: careerMode = True quickPlayMode = False else: careerMode = False #if gameMode1p == 0: #akedrou - different sorting for practice mode and quickplay mode?? quickPlayMode = True #else: # quickPlayMode = False quickPlayCareerTiers = engine.config.get("game", "quickplay_tiers") if quickPlayMode and quickPlayCareerTiers == 0: if direction == 0: if order == 1: return cmp(a.artist.lower(), b.artist.lower()) elif order == 2: return cmp(int(b.count+str(0)), int(a.count+str(0))) elif order == 0: return cmp(removeSongOrderPrefixFromName(a.name).lower(), removeSongOrderPrefixFromName(b.name).lower()) elif order == 3: return cmp(a.album.lower(), b.album.lower()) elif order == 4: return cmp(a.genre.lower(), b.genre.lower()) elif order == 5: return cmp(a.year.lower(), b.year.lower()) elif order == 6: return cmp(a.diffSong, b.diffSong) elif order == 7: return cmp(theInstrumentDiff(a), theInstrumentDiff(b)) elif order == 8: return cmp(a.icon.lower(), b.icon.lower()) else: if order == 1: return cmp(b.artist.lower(), a.artist.lower()) elif order == 2: return cmp(int(a.count+str(0)), int(b.count+str(0))) elif order == 0: return cmp(removeSongOrderPrefixFromName(b.name).lower(), removeSongOrderPrefixFromName(a.name).lower()) elif order == 3: return cmp(b.album.lower(), a.album.lower()) elif order == 4: return cmp(b.genre.lower(), a.genre.lower()) elif order == 5: return cmp(b.year.lower(), a.year.lower()) elif order == 6: return cmp(b.diffSong, a.diffSong) elif order == 7: return cmp(theInstrumentDiff(a), theInstrumentDiff(b)) elif order == 8: return cmp(b.icon.lower(), a.icon.lower()) elif gameMode1p != 2 and quickPlayCareerTiers == 2: Aval = "" Bval = "" if isinstance(a, SongInfo): if order == 0: Aval = removeSongOrderPrefixFromName(a.name)[0].lower() if Aval.isdigit(): Aval = "123" if not Aval.isalnum(): Aval = "!@#" elif order == 1: Aval = a.artist.lower() elif order == 2: Aval = int(a.count+str(0)) if Aval == "": Aval = "0" elif order == 3: Aval = a.album.lower() elif order == 4: Aval = a.genre.lower() elif order == 5: Aval = a.year.lower() elif order == 6: Aval = a.diffSong elif order == 7: Aval = theInstrumentDiff(a) elif order == 8: Aval = a.icon.lower() elif isinstance(a, SortTitleInfo): if order == 2: Aval = int(a.name+str(0)) elif order == 6 or order == 7: Aval = int(a.name) else: Aval = a.name.lower() if isinstance(b, SongInfo): if order == 0: Bval = removeSongOrderPrefixFromName(b.name)[0].lower() if Bval.isdigit(): Bval = "123" if not Bval.isalnum(): Bval = "!@#" elif order == 1: Bval = b.artist.lower() elif order == 2: Bval = int(b.count+str(0)) if Bval == "": Bval = "0" elif order == 3: Bval = b.album.lower() elif order == 4: Bval = b.genre.lower() elif order == 5: Bval = b.year.lower() elif order == 6: Bval = b.diffSong elif order == 7: Bval = theInstrumentDiff(b) elif order == 8: Bval = b.icon.lower() elif isinstance(b, SortTitleInfo): if order == 2: Bval = int(b.name+str(0)) elif order == 6 or order == 7: Bval = int(b.name) else: Bval = b.name.lower() if Aval != Bval: #MFH - if returned unlock IDs are different, sort by unlock ID (this roughly sorts the tiers and shoves "bonus" songs to the top) if direction == 0: return cmp(Aval, Bval) else: return cmp(Bval, Aval) elif isinstance(a, SortTitleInfo) and isinstance(b, SortTitleInfo): return 0 elif isinstance(a, SortTitleInfo) and isinstance(b, SongInfo): return -1 elif isinstance(a, SongInfo) and isinstance(b, SortTitleInfo): return 1 else: return cmp(a.name, b.name) else: #Log.debug("Unlock IDs found, a=" + str(a.getUnlockID()) + ", b=" + str(b.getUnlockID()) ) if a.getUnlockID() == "" and b.getUnlockID() != "": #MFH - a is a bonus song, b is involved in career mode return 1 elif b.getUnlockID() == "" and a.getUnlockID() != "": #MFH - b is a bonus song, a is involved in career mode - this is fine. return -1 elif a.getUnlockID() == "" and b.getUnlockID() == "": #MFH - a and b are both bonus songs; apply sorting logic. #MFH: catch BlankSpaceInfo ("end of career") marker, ensure it goes to the top of the bonus songlist if isinstance(a, SongInfo) and isinstance(b, BlankSpaceInfo): #a is a bonus song, b is a blank space (end of career marker) return 1 elif isinstance(a, BlankSpaceInfo) and isinstance(b, SongInfo): #a is a blank space, b is a bonus song (end of career marker) - this is fine. return -1 else: #both bonus songs, apply sort order: if direction == 0: if order == 1: return cmp(a.artist.lower(), b.artist.lower()) elif order == 2: return cmp(int(b.count+str(0)), int(a.count+str(0))) elif order == 0: return cmp(removeSongOrderPrefixFromName(a.name).lower(), removeSongOrderPrefixFromName(b.name).lower()) elif order == 3: return cmp(a.album.lower(), b.album.lower()) elif order == 4: return cmp(a.genre.lower(), b.genre.lower()) elif order == 5: return cmp(a.year.lower(), b.year.lower()) elif order == 6: return cmp(a.diffSong, b.diffSong) elif order == 7: return cmp(theInstrumentDiff(a), theInstrumentDiff(b)) elif order == 8: return cmp(a.icon.lower(), b.icon.lower()) else: if order == 1: return cmp(b.artist.lower(), a.artist.lower()) elif order == 2: return cmp(int(a.count+str(0)), int(b.count+str(0))) elif order == 0: return cmp(removeSongOrderPrefixFromName(b.name).lower(), removeSongOrderPrefixFromName(a.name).lower()) elif order == 3: return cmp(b.album.lower(), a.album.lower()) elif order == 4: return cmp(b.genre.lower(), a.genre.lower()) elif order == 5: return cmp(b.year.lower(), a.year.lower()) elif order == 6: return cmp(b.diffSong, a.diffSong) elif order == 7: return cmp(theInstrumentDiff(b), theInstrumentDiff(a)) elif order == 8: return cmp(b.icon.lower(), a.icon.lower()) #original career sorting logic: elif a.getUnlockID() != b.getUnlockID(): #MFH - if returned unlock IDs are different, sort by unlock ID (this roughly sorts the tiers and shoves "bonus" songs to the top) return cmp(a.getUnlockID(), b.getUnlockID()) elif isinstance(a, TitleInfo) and isinstance(b, TitleInfo): return 0 elif isinstance(a, TitleInfo) and isinstance(b, SongInfo): return -1 elif isinstance(a, SongInfo) and isinstance(b, TitleInfo): return 1 else: #MFH - This is where career songs are sorted within tiers -- we want to force sorting by "name" only: return cmp(a.name.lower(), b.name.lower()) #MFH - force sort by name for career songs def isInt(possibleInt): try: #MFH - remove any leading zeros (for songs with 01. or 02. for example) splitName = possibleInt.split("0",1) while splitName[0] == "": splitName = possibleInt.split("0",1) if len(splitName) > 1: if splitName[0] == "": possibleInt = splitName[1] if str(int(possibleInt)) == str(possibleInt): return True else: return False except Exception, e: return False def removeSongOrderPrefixFromName(name): #copied from Dialogs - can't import it here. if not name.startswith("."): splitName = name.split(".",1) if isInt(splitName[0]) and len(splitName) > 1: name = splitName[1] splitName[0] = "" while splitName[0] == "": splitName = name.split(" ",1) if len(splitName) > 1: if splitName[0] == "": name = splitName[1] return name