import string


## CLASS TROMMLER.SONG:
## ====================
## Stores a whole Trommler song
class TrommlerSong:

  ## CLASS ATTRIBUTES:
  ## -----------------
  ## Prefix for whole file
  filePrefix = """# 
# Songfile created by Drummer
#"""
  ## Prefix for drum samples list
  drumPrefix="""# 
# Drums
#"""
  ## Prefix for pattern list
  patternPrefix = """# 
# Patterns
#"""
  ## Prefix for song description list
  songPrefix = """# 
# Song
#"""
  ## "End of file" marker
  eofMarker = """#
# eof
#"""
  ## Song description output mask
  outputMask = 's "%(title)s" %(tempo)4d %(steps)4d'
  ## Maximum numbers of samples and patterns
  maxSamples = 32
  maxPatterns = 128

  
  ## METHOD __INIT__:
  ## ----------------
  # Creates a song
  def __init__(self, **options):
    ## Default values
    self.carDict = {}
    self.carDict['title'] = ''
    self.carDict['tempo'] = 60
    self.carDict['steps'] = 0
    ## If there's a file, read it
    file2Load = None
    if options.has_key('filename'):
      if options['filename'] <> None:
        file2Load = TrommlerFile(options['filename'])
        ## Get characteristics from file
        self.carDict['title'] = file2Load.getTitle()
        self.carDict['tempo'] = file2Load.getTempo()
        self.carDict['steps'] = file2Load.getSteps()
    ## If no file, get characteristics from options
    if file2Load == None:
      if options.has_key('title'): self.carDict['title'] = options['title']
      if options.has_key('tempo'): self.carDict['tempo'] = options['tempo']
      if options.has_key('steps'): self.carDict['steps'] = options['steps']
    ## Create drum samples
    self.sampleList = []
    for s in range(TrommlerSong.maxSamples):
      ## If loading a file, read from file
      if file2Load <> None:
        samp = file2Load.getDrumSample(s)
      ## Else create a new sample
      else:
        samp = TrommlerDrumSample(s, 'Drum %d' % s, 'sample%d.smp' % s, 0)
      ## Add sample
      self.sampleList.append(samp)
    ## Create patterns
    self.patternList = []
    for p in range(TrommlerSong.maxPatterns):
      ## If loading a file, get pattern from file
      if file2Load <> None:
        patt = file2Load.getPattern(p)
      ## Otherwise, create new pattern
      else:
        patt = TrommlerPattern(p, 'Pattern %d' % p, 4, 4)
      ## Add to pattern list
      self.patternList.append(patt)
    ## Creates steps in song
    self.songPatterns = []
    for s in range(self.carDict['steps']):
      ## If loading a file, get step from file
      step = 0
      if file2Load <> None: step = file2Load.getStep(s)
      ## Add to steps
      self.songPatterns.append(step)
      
      
  ## METHOD ADD.DRUM.SAMPLE:
  ## -----------------------
  # Adds a drum sample to the song
  def addDrumSample(self, sample):
    sampleNum = sample.carDict['number']
    if sampleNum < 0 or sampleNum >= TrommlerSong.maxSamples:
      raise ValueError, "Invalid sample number: %d" % sampleNum
    self.sampleList[sampleNum] = sample
    
    
  ## METHOD ADD.PATTERN:
  ## -------------------
  # Adds a pattern to the pattern list for the song
  def addPattern(self, pattern):
    patternNum = pattern.getNumber()
    if patternNum < 0 or patternNum >= TrommlerSong.maxPatterns:
      raise ValueError, "Invalid pattern number: %d" % patternNum
    self.patternList[patternNum] = pattern
    
    
  ## METHOD SET.STEP:
  ## ----------------
  # Sets a step in the song
  def setStep(self, step, patternNum):
    if step < 0 or step >= self.carDict['steps']:
      raise ValueError, "Invalid step number: %d" % step
    self.songPatterns[step] = patternNum
  
  
  ## METHODS GET.---:
  ## ----------------
  # Return song features
  def getTitle(self): return self.carDict['title']
  def getTempo(self): return self.carDict['tempo']
  def getSteps(self): return self.carDict['steps']
  def getSample(self, sampleNum): return self.sampleList[sampleNum]
  def getPattern(self, pattNum): return self.patternList[pattNum]
  def getStep(self, stepNum): return self.songPatterns[stepNum]
  
  
  ## METHOD __REPR__:
  ## ----------------
  # String representation for song
  def __repr__(self):
    repr = TrommlerSong.filePrefix
    ## Insert samples
    repr = repr + '\n' + TrommlerSong.drumPrefix
    for samp in self.sampleList:
      repr = repr + '\n' + `samp`
    ## Insert patterns
    repr = repr + '\n' + TrommlerSong.patternPrefix
    for patt in self.patternList:
      repr = repr + '\n' + `patt`
    ## Insert song itself
    repr = repr + '\n' + TrommlerSong.outputMask % self.carDict
    for step in self.songPatterns:
      repr = repr + '\n' + "%d" % step
    
    return repr
    


## CLASS TROMMLER.FILE:
## ====================
# Reads a Trommler song file
class TrommlerFile:

  ## METHOD __INIT__:
  ## ----------------
  # Reads the file
  def __init__(self, fileName):
    ## Initialize song features
    self.title = ''
    self.tempo = 60
    self.steps = []
    self.drumSamples = [ None ] * TrommlerSong.maxSamples
    self.patterns = [ None ] * TrommlerSong.maxPatterns
    ## Open the file for reading
    file = open(fileName)
    ## Browse lines in file
    inSteps = 0
    line = file.readline()
    while line <> '':
      ## If in song steps
      if inSteps:
        ## Try to convert line into integer
        step = -1
        try: step = string.atoi(line[:-1])
        except: pass
        ## If succeeded, add step to song
        if step <> -1: self.steps.append(step)
      ## If not in steps
      else:
        ## Line beginning with 'd': drum sample
        if line[0] == 'd':
          ## Split the line
          words = self.splitWQuotes(line[:-1])
          ## Line contains sample number, sample name, sample file name and volume
          sampNum = string.atoi(words[1])
          sampName = words[2][1:-1]
          sampFile = words[3][1:-1]
          sampVol = string.atoi(words[4])
          ## Create and insert sample
          self.drumSamples[sampNum] = TrommlerDrumSample(sampNum, sampName, sampFile, sampVol)
        ## If line beginning with 'p', pattern
        if line[0] == 'p':
          ## Split the line
          words = self.splitWQuotes(line[:-1])
          ## Line contains pattern number, pattern name, units and BPUs
          pattNum = string.atoi(words[1])
          pattName = words[2][1:-1]
          pattUnits = string.atoi(words[3])
          pattBPUs = string.atoi(words[4])
          ## Create and insert pattern
          self.patterns[pattNum] = TrommlerPattern(pattNum, pattName, pattUnits, pattBPUs)
        ## If line beginning with 'b', beats in current sample
        if line[0] == 'b':
          ## Split the line
          words = self.splitWQuotes(line[:-1])
          ## Line contains pattern number, instrument number and beats
          pattNum = string.atoi(words[1])
          instrNum = string.atoi(words[2])
          beats = words[3]
          ## Beat is set for each '*' in beats
          for b in range(len(beats)):
            if beats[b] == '*': self.patterns[pattNum].addNote(instrNum, b)
        ## If line beginning with 's', song features
        if line[0] == 's':
          ## Split the line
          words = self.splitWQuotes(line[:-1])
          ## Line contains song title and tempo
          self.title = words[1][1:-1]
          self.tempo = string.atoi(words[2])
          ## Next lines will be song steps
          inSteps = 1
      ## Next line
      line = file.readline()
    ## Close file
    file.close()
  
  
  ## METHOD SPLIT.W.QUOTES:
  ## ----------------------
  # Splits a string taking into account quoted parts
  def splitWQuotes(self, str):
    ## List for result
    words = []
    ## Not in quoted part
    inQuote = None
    ## Parse string characters
    spaceBefore = 0
    curWord = ''
    for c in str:
      ## If in quoted part
      if inQuote <> None:
        ## Always add charchater
        curWord = curWord + c
        ## If closing quote, close quoted part
        if c == inQuote: inQuote = None
      ## If not in quoted part
      else:
        ## If not space
        if c <> ' ':
          ## If preceding character was a space, new word
          if spaceBefore:
            curWord = c
            ## If c is a quote or double-quote, entering quoted part
            if c == '"' or c == "'": inQuote = c
          ## If preceding character wasn't a space, add to current word
          else:
            curWord = curWord + c
          ## It wasn't a space
          spaceBefore = 0
        ## If current character is a space
        else:
          ## Add current word to list if needed
          if not spaceBefore: words.append(curWord)
          ## Remember it
          spaceBefore = 1
    ## End of string: add last word if needed
    if not spaceBefore: words.append(curWord)
    ##  Return words
    return words
    
    
  ## METHOD GET.TITLE:
  ## -----------------
  # Return the title of the song
  def getTitle(self):
    return self.title
    
    
  ## METHOD GET.TEMPO:
  ## -----------------
  # Return the tempo for the song
  def getTempo(self):
    return self.tempo
    
    
  ## METHOD GET.STEPS:
  ## -----------------
  # Return the number of steps in the song the song
  def getSteps(self):
    return len(self.steps)
    
    
  ## METHOD GET.DRUM.SAMPLE:
  ## -----------------------
  # Return a drum sample in the song
  def getDrumSample(self, sampNum):
    return self.drumSamples[sampNum]
    
    
  ## METHOD GET.PATTERN:
  ## -------------------
  # Return a pattern in the song
  def getPattern(self, pattNum):
    return self.patterns[pattNum]
    
    
  ## METHOD GET.STEP:
  ## ----------------
  # Return a step in the song
  def getStep(self, stepNum):
    return self.steps[stepNum]



## CLASS TROMMLER.DRUM.SAMPLE:
## ===========================
# Stores a drum sample description for a Trommler song
class TrommlerDrumSample:

  ## CLASS ATTRIBUTES:
  ## -----------------
  outputMask = 'd  %(number)2d "%(name)s" "%(file)s" %(volume)3d   0'
  
  
  ## METHOD __INIT__:
  ## ----------------
  # Creates a drum sample
  def __init__(self, number, name, file, volume):
    self.carDict = {}
    self.carDict['number'] = number
    self.carDict['name']   = name
    self.carDict['file']   = file
    self.carDict['volume'] = volume
  
  ## METHODS GET.---:
  ## ----------------
  # Return sample features
  def getNumber(self): return self.carDict['number']
  def getName(self): return self.carDict['name']
  def getFile(self): return self.carDict['file']
  def getVolume(self): return self.carDict['volume']
  
  
  ## METHOD __REPR__:
  ## ----------------
  # String representation of the sample
  def __repr__(self):
    return TrommlerDrumSample.outputMask % self.carDict
    


## CLASS TROMMLER.PATTERN:
## =======================
# Stores a pattern for a Trommler song
class TrommlerPattern:

  ## CLASS ATTRIBUTES:
  ## -----------------
  beginOutputMask = 'p %(number)3d "%(name)s"  %(units)2d   %(bpus)2d'
  bodyOutputMask  = 'b %(number)3d  %(instrnum)2d %(notes)s'
  
  ## METHOD __INIT__:
  ## ----------------
  # Creates a pattern
  def __init__(self, number, name, units, bpus):
    ## Store characteristics
    self.carDict = {}
    self.carDict['number'] = number
    self.carDict['name']   = name
    self.carDict['units']  = units
    self.carDict['bpus']   = bpus
    ## Create beats
    self.beats = []
    voidLine = '.' * (units * bpus)
    for samp in range(TrommlerSong.maxSamples):
      self.beats.append({'empty':1, 'number':number, 'instrnum':samp, 'notes':voidLine})
    
  ## METHOD ADD.NOTE:
  ## ----------------
  # Adds a note to a pattern for an instrument
  def addNote(self, instrnum, noteNum):
    ## Beat is no more empty
    self.beats[instrnum]['empty'] = 0
    ## Insert note
    oldNotes = self.beats[instrnum]['notes']
    self.beats[instrnum]['notes'] = oldNotes[:noteNum] + '*' + oldNotes[noteNum + 1:]
    
  ## METHODS GET.---:
  ## ----------------
  # Get pattern features
  def getNumber(self): return self.carDict['number']
  def getName(self): return self.carDict['name']
  def getUnits(self): return self.carDict['units']
  def getBPUs(self): return self.carDict['bpus']
  def getBeat(self, instrNum, beatNum):
    ## Look up beats for given instrument
    beats = None
    for b in self.beats:
      if b['instrnum'] == instrNum: beats = b
    ## If not found, no beat
    if beats == None: return 0
    ## Beat set if corresponding char is '*'
    return (beats['notes'][beatNum] == '*')
    
  ## METHOD IS.SAME:
  ## ---------------
  # Compares if two patterns have the same notes
  def isSame(self, patt):
    for samp in range(TrommlerSong.maxSamples):
      if self.beats[samp]['notes'] <> patt.beats[samp]['notes']: return 0
    return 1
    
  ## METHOD __REPR__:
  ## ----------------
  # String representation of the pattern
  def __repr__(self):
    repr = TrommlerPattern.beginOutputMask % self.carDict
    for b in self.beats:
      if not b['empty']:
        repr = repr + '\n' + TrommlerPattern.bodyOutputMask % b
    
    return repr


if __name__ == '__main__':
  sng = TrommlerSong('My song', 135, 16)
  smp0 = TrommlerDrumSample(0, 'TR909 kick', '/tmp/tr-kick.smp', 100)
  sng.addDrumSample(smp0)
  smp1 = TrommlerDrumSample(1, 'TR909 cl. hi-hat', '/tmp/tr-chh.smp', 100)
  sng.addDrumSample(smp1)
  smp2 = TrommlerDrumSample(2, 'TR909 op. hi-hat', '/tmp/tr-ohh.smp', 100)
  sng.addDrumSample(smp2)
  pat0 = TrommlerPattern(0, '4 2 the floor', 4, 4);
  pat0.addNote(0,  0)
  pat0.addNote(0,  4)
  pat0.addNote(0,  8)
  pat0.addNote(0, 12)
  sng.addPattern(pat0)
  pat1 = TrommlerPattern(1, 'Kick+HH/1', 4, 4);
  pat1.addNote(0,  0)
  pat1.addNote(0,  4)
  pat1.addNote(0,  8)
  pat1.addNote(0, 12)
  for i in range(14): pat1.addNote(1, i)
  pat1.addNote(2, 14)
  sng.addPattern(pat1)
  pat2 = TrommlerPattern(2, 'Kick+HH/2', 4, 4);
  pat2.addNote(0,  0)
  pat2.addNote(0,  4)
  pat2.addNote(0,  8)
  pat2.addNote(0, 12)
  for i in range(16): pat2.addNote(1, i)
  sng.addPattern(pat2)
  for i in range(4, 16, 2):
    sng.setStep(i, 1)
    sng.setStep(i+1, 2)
  print sng
    
