mirror of
https://github.com/microtherion/VocalEasel.git
synced 2024-12-22 11:14:00 +00:00
502 lines
17 KiB
Python
502 lines
17 KiB
Python
|
|
# patPlectrum.py
|
|
|
|
"""
|
|
This module is an integeral part of the program
|
|
MMA - Musical Midi Accompaniment.
|
|
|
|
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
Bob van der Poel <bob@mellowood.ca>
|
|
Louis James Barman
|
|
|
|
"""
|
|
|
|
|
|
import MMA.notelen
|
|
import MMA.harmony
|
|
|
|
import gbl
|
|
from MMA.common import *
|
|
from MMA.pat import PC
|
|
|
|
|
|
class Plectrum(PC):
|
|
""" Pattern class for a Raw MIDI track. """
|
|
|
|
|
|
vtype = 'PLECTRUM'
|
|
|
|
def __init__(self, nm):
|
|
PC.__init__(self, nm)
|
|
|
|
# We have vibrating strings (a string in python refers to text not a guitar string)
|
|
self._vibrating = []
|
|
self._tuning = []
|
|
self._capoFretNo = 0 # The number that the capo is on (0 for open strings)
|
|
self.setPlectrumTuning(['e-', 'a-', 'd', 'g', 'b', 'e+'])
|
|
|
|
def saveGroove(self, gname):
|
|
""" Save special/local variables for groove. """
|
|
|
|
PC.saveGroove(self, gname) # create storage. Do this 1st.
|
|
self.grooves[gname]['CAPO'] = self._capoFretNo
|
|
|
|
def restoreGroove(self, gname):
|
|
""" Restore special/local/variables for groove. """
|
|
|
|
self._capoFretNo = self.grooves[gname]['CAPO']
|
|
PC.restoreGroove(self, gname)
|
|
|
|
|
|
def clearSequence(self):
|
|
""" Set some initial values. Called from init and clear seq. """
|
|
|
|
PC.clearSequence(self)
|
|
self._capoFretNo = 0
|
|
if self.channel != 0: # not sure if this is nesc. But safer!
|
|
self.grooveFinish(0)
|
|
|
|
|
|
def doMidiClear(self):
|
|
""" Reset MIDI settings, special hook for stopping strings. """
|
|
|
|
if self.channel != 0:
|
|
self.grooveFinish(0)
|
|
PC.doMidiClear(self)
|
|
|
|
|
|
def setPlectrumTuning (self, stringPitchNames):
|
|
""" This is called from the parser TUNING option to set the
|
|
instrument tuning.
|
|
|
|
For standard guitar tuning use setTuning("e- a- d g b e+")
|
|
|
|
"""
|
|
|
|
if self.channel != 0: # if tuning changes while strings are still sounding
|
|
self.grooveFinish(0)
|
|
|
|
self._tuning=[]
|
|
self._vibration=[]
|
|
|
|
for pitchName in stringPitchNames:
|
|
midiPitch = noteNameToMidiPitch(pitchName)
|
|
if midiPitch == None:
|
|
error("%s TUNING: Illegal/unknown string name '%s'." % (self.name, pitchName))
|
|
|
|
self._tuning.append(midiPitch)
|
|
vibration = struct()
|
|
vibration.note = None # None means the string not vibrating
|
|
self._vibrating.append(vibration)
|
|
|
|
def setPlectrumCapo (self, capoFretNo):
|
|
""" Set a capo value. Called from main parser. """
|
|
|
|
self._capoFretNo = stoi(capoFretNo, "%s Capo: expecting integer, not '%s'." \
|
|
% (self.name, capoFretNo))
|
|
|
|
def decodePlectrumPatterns(self, a, patterns) :
|
|
""" Decode plectrum patterns for a guitar here are examples """
|
|
|
|
a.pluckVol = []
|
|
if patterns[0].find(':') != -1 or len(patterns) == 1 :
|
|
# set all strings to note plucked
|
|
for stringNo in range(len(self._tuning)):
|
|
a.pluckVol.append(-1)
|
|
|
|
if len(patterns) == 1 and patterns[0].find(':') == -1:
|
|
# put in the missing : if there is just one pattern
|
|
patterns[0] = ":" + patterns[0]
|
|
|
|
for patString in patterns:
|
|
if patString.find(':') == -1:
|
|
error("%s: Not all string definitions have a : in them '%s'"% \
|
|
(self.name, ' '.join(patterns)))
|
|
|
|
start = 0
|
|
end = len(self._tuning) - 1
|
|
|
|
pat = patString.split(':')
|
|
|
|
if pat[0]:
|
|
emsg = "%s: Note String Number in Plectrum definition not int" % self.name
|
|
if pat[0].find('-') != -1:
|
|
startString, endString = pat[0].split('-')
|
|
if startString:
|
|
start = stoi(startString, emsg) - 1
|
|
if endString:
|
|
end = stoi(endString, emsg) - 1
|
|
else:
|
|
start = stoi(pat[0], emsg) - 1
|
|
end = start
|
|
|
|
if start> end :
|
|
start, end = end , start
|
|
|
|
for n in range(start, end + 1):
|
|
# we number strings from the other end
|
|
stringNo = len(self._tuning) - n - 1
|
|
if stringNo <0 or stringNo >= len(self._tuning):
|
|
error("%s: string number %d does not exists" % (self.name, stringNo + 1))
|
|
return
|
|
if a.pluckVol[stringNo] != -1:
|
|
error("%s: Duplicate string %d definition" % (self.name, stringNo + 1))
|
|
return
|
|
|
|
a.pluckVol[stringNo] = stoi(pat[1], "%s: Note volume not int" % self.name)
|
|
|
|
|
|
else:
|
|
# this is without a the string specifier so all strings must be listed.
|
|
if len(patterns ) != len(self._tuning):
|
|
error("%s: There must be %s strings listed definition, "
|
|
"not '%s'" % (self.name, len(self._tuning), ' '.join(patterns)) )
|
|
return
|
|
for stringNo in range(len(self._tuning)):
|
|
val = patterns[stringNo]
|
|
if val=='-':
|
|
val = '-1' # -1 means ignore this string
|
|
a.pluckVol.append(stoi(val, "%s: Note volume not int" % self.name))
|
|
|
|
def getPgroup(self, ev):
|
|
""" Get group for rawmid pattern.
|
|
|
|
Fields - start, length, note, volume
|
|
|
|
"""
|
|
|
|
if len(ev) < 3: # we need offset, strum and at least one pattern
|
|
error("%s: There must be n groups of 3 or more in a pattern definition, "
|
|
"not '%s'" % (self.name, ' '.join(ev) ) )
|
|
|
|
a = struct()
|
|
|
|
a.vol = 0 # as is this
|
|
a.offset = self.setBarOffset(ev[0])
|
|
a.strum = stoi(ev[1], "%s: Expecting int value for strum" % self.name)
|
|
|
|
self.decodePlectrumPatterns(a, ev[2:] )
|
|
|
|
""" For the doc generators ... we need a setting for duration, even
|
|
though this track doesn't really have one. Using the value of 0
|
|
pretty much matches the results for a drum track. The author of
|
|
this file used "pluckVol" and the docs are expecting a "vol" variable
|
|
so we just duplicate it ... easier than changing all the uses here.
|
|
"""
|
|
|
|
a.duration = 0 # this is a dummy value to keep docs happy
|
|
a.vol = a.pluckVol
|
|
|
|
return a
|
|
|
|
|
|
def restart(self):
|
|
self.ssvoice = -1
|
|
|
|
|
|
|
|
def endVibration(self, stringNo, offset):
|
|
""" kill the vibration on the string by sending out a note off """
|
|
|
|
# first test if this string has been played before
|
|
if self._vibrating[stringNo].note == None:
|
|
return
|
|
|
|
vibration = self._vibrating[stringNo]
|
|
|
|
gbl.mtrks[self.channel].addNoteOnToTrack( offset, vibration.note, 0) # v=0 ==note off
|
|
self._vibrating[stringNo].note = None
|
|
|
|
|
|
def grooveFinish(self, offset):
|
|
""" End all vibrations (ie output all outstanding note off). """
|
|
|
|
for stringNo in range(len(self._vibrating)):
|
|
self.endVibration(stringNo, offset)
|
|
|
|
def fretboardNote(self, stringNo, chordList, startIdx, previousFret, chordBarreFretNo):
|
|
""" Returns a single note that is is on the chord for one string
|
|
Unlike a guitar string number the lowest string is string Number 0
|
|
"""
|
|
|
|
openString = self._tuning[stringNo] + self._capoFretNo + chordBarreFretNo
|
|
|
|
fretNotes = {}
|
|
for n,note in enumerate(chordList[startIdx:]):
|
|
fretNo = note - openString % 12
|
|
while fretNo < 0:
|
|
fretNo += 12
|
|
fretNo %= 12
|
|
|
|
fret = struct()
|
|
fret.pitch = self.adjustNote(openString + fretNo)
|
|
fret.fretNo = fretNo
|
|
fret.chordIndex = n + startIdx
|
|
fretNotes[fretNo] = fret
|
|
|
|
# Try to guess where the next finger position in the chord goes
|
|
# without trying to stretch the hand too much.
|
|
if previousFret :
|
|
if fretNotes.has_key(previousFret.fretNo):
|
|
return fretNotes[previousFret.fretNo]
|
|
if fretNotes.has_key(previousFret.fretNo -1):
|
|
return fretNotes[previousFret.fretNo -1]
|
|
if fretNotes.has_key(previousFret.fretNo -2):
|
|
return fretNotes[previousFret.fretNo -2]
|
|
if fretNotes.has_key(previousFret.fretNo + 1) and previousFret.fretNo <= 3:
|
|
return fretNotes[previousFret.fretNo + 1]
|
|
|
|
lowest = min(fretNotes.keys())
|
|
|
|
return fretNotes[lowest]
|
|
|
|
|
|
def fretboardNotes(self, chordList, chordBarreFretNo):
|
|
notes = []
|
|
|
|
if len(chordList) >= 5 or len(self._tuning) >= 8:
|
|
# I don't know how to handle five or more notes in a chord
|
|
# or more than 8 strings (eg a harp)
|
|
# so just do a single pass and see what happens
|
|
previousFret = None
|
|
for stringNo in range(len(self._tuning)):
|
|
fret = self.fretboardNote( stringNo, chordList, 0, previousFret, chordBarreFretNo)
|
|
previousFret = fret
|
|
notes.append(fret)
|
|
|
|
else:
|
|
#find the triad chord (the first three notes of the chord) first
|
|
previousFret = None
|
|
for stringNo in range(len(self._tuning)):
|
|
fret = self.fretboardNote( stringNo, chordList[:3], 0,
|
|
previousFret, chordBarreFretNo)
|
|
previousFret = fret
|
|
notes.append(fret)
|
|
|
|
|
|
|
|
if len(chordList) == 4:
|
|
# Now put in missing 7th (or what ever it is called) but
|
|
# start searching from the top string
|
|
|
|
for stringNo in reversed(range(len(self._tuning))):
|
|
|
|
fret = self.fretboardNote( stringNo, chordList, 3, None, chordBarreFretNo)
|
|
if fret.fretNo <= 4:
|
|
notes[stringNo] = fret
|
|
|
|
# now go back up to the top string to make sure there is no bunching
|
|
stringNo += 1
|
|
while stringNo < len(self._tuning):
|
|
notes[stringNo] = self.fretboardNote( stringNo, chordList, 0,
|
|
None, chordBarreFretNo)
|
|
stringNo += 1
|
|
break
|
|
|
|
|
|
# look for and mark duplicates
|
|
notes[0].duplicate = False
|
|
for stringNo in range(1, len(self._tuning)):
|
|
if notes[stringNo -1].pitch == notes[stringNo].pitch:
|
|
notes[stringNo].duplicate = True
|
|
else:
|
|
notes[stringNo].duplicate = False
|
|
|
|
return notes
|
|
|
|
|
|
|
|
def trackBar(self, pattern, ctable):
|
|
""" Do a plectrum bar.
|
|
|
|
Called from self.bar()
|
|
|
|
"""
|
|
|
|
sc = self.seq
|
|
|
|
for p in pattern:
|
|
try:
|
|
ct = self.getChordInPos(p.offset, ctable)
|
|
chordList=ct.chord.noteList # catch the case when there is no noteList attribute
|
|
except AttributeError:
|
|
continue
|
|
|
|
if ct.plectrumZ:
|
|
continue
|
|
|
|
if len(self._tuning) != len(p.pluckVol):
|
|
error("%s: Pattern and tuning lengths (%s, %s) do not match. "
|
|
"Was tuning changed?" % (self.name, len(p.pluckVol), len(self._tuning)))
|
|
|
|
chordBarreFretNo = 0
|
|
if ct.name.startswith('+'):
|
|
chordBarreFretNo += 12
|
|
if ct.name.startswith('-'):
|
|
chordBarreFretNo += -12
|
|
|
|
chordBarreFretNo += ct.chord.barre
|
|
|
|
|
|
if gbl.debug or gbl.plecShow:
|
|
self.printChordShape(ct, chordBarreFretNo)
|
|
|
|
plectrumNoteOnList = [] # for debugging only
|
|
|
|
# Find how many strings have been plucked this time
|
|
pluckStringCount = 0;
|
|
for vol in p.pluckVol:
|
|
if vol == -1:
|
|
continue
|
|
pluckStringCount += 1
|
|
|
|
pluckStringIndex = 0;
|
|
|
|
notes = self.fretboardNotes(chordList, chordBarreFretNo)
|
|
|
|
for stringNo, vol in enumerate(p.pluckVol):
|
|
|
|
# the centre of the strum is on the beat
|
|
strumOffset = p.offset + p.strum*(pluckStringIndex - pluckStringCount/2.0)
|
|
|
|
if vol == -1:
|
|
# silence this stringNo if the note on this string has changed
|
|
# even if this stringNo has not been plucked or muted
|
|
if notes[stringNo].pitch != self._vibrating[stringNo].note:
|
|
self.endVibration(stringNo, strumOffset)
|
|
|
|
continue # this string has not been plucked or damped
|
|
|
|
pluckStringIndex += 1
|
|
|
|
self.endVibration(stringNo, strumOffset)
|
|
if vol >= 1:
|
|
|
|
note = notes[stringNo].pitch
|
|
|
|
if notes[stringNo].duplicate:
|
|
if gbl.debug:
|
|
print "%s: Ignoring duplicate note %d." % (self.name, note)
|
|
continue
|
|
|
|
outputVolume = self.adjustVolume(vol, p.offset)
|
|
|
|
gbl.mtrks[self.channel].addNoteOnToTrack(strumOffset, note,
|
|
outputVolume, self.rTime[sc][0], self.rTime[sc][1] )
|
|
|
|
self._vibrating[stringNo].note = note
|
|
if outputVolume == 0:
|
|
self._vibrating[stringNo].note = None
|
|
|
|
plectrumNoteOnList.append(note) # for debugging only
|
|
|
|
if gbl.debug:
|
|
print "%s: channel=%s offset=%s chordList=%s NoteOn=%s." % \
|
|
(self.name, self.channel, p.offset + gbl.tickOffset, \
|
|
chordList, plectrumNoteOnList )
|
|
|
|
|
|
|
|
def printChordShape(self, chordTable, chordBarreFretNo = 0):
|
|
|
|
chordList=chordTable.chord.noteList
|
|
|
|
# catch the case when there is no noteList attribute
|
|
if hasattr(self, 'previousChordList'):
|
|
if chordList == self.previousChordList and \
|
|
chordBarreFretNo == self.previousFretNo:
|
|
return
|
|
self.previousChordList = chordList
|
|
self.previousFretNo = chordBarreFretNo
|
|
|
|
notes = self.fretboardNotes(chordList, chordBarreFretNo)
|
|
notes.reverse()
|
|
|
|
printStart = 0
|
|
startFretNo = self._capoFretNo + chordBarreFretNo
|
|
if startFretNo < 0:
|
|
printStart = startFretNo
|
|
|
|
|
|
print
|
|
print self.name,chordTable.name, " chord ", chordList
|
|
for stringNo, openNote in enumerate(reversed(self._tuning)):
|
|
openNote = self.adjustNote(openNote) # puts into middle octave 60==5*12
|
|
note = notes[stringNo].pitch
|
|
|
|
print "%s %3d" % (self.name, openNote + self._capoFretNo),
|
|
finger = note - openNote
|
|
for fretNo in range (printStart, 20):
|
|
if fretNo == 0 and self._capoFretNo == 0:
|
|
print "|",
|
|
elif fretNo == self._capoFretNo and chordBarreFretNo == 0:
|
|
print "$",
|
|
elif fretNo == finger:
|
|
print "*",
|
|
elif fretNo == self._capoFretNo:
|
|
print "$",
|
|
elif fretNo == startFretNo:
|
|
print ":",
|
|
elif fretNo == 0:
|
|
print "|",
|
|
else:
|
|
print "-",
|
|
|
|
print "%d %d"% ( notes[stringNo].chordIndex, note, ),
|
|
if notes[stringNo].duplicate:
|
|
print " duplicate",
|
|
|
|
print
|
|
|
|
print
|
|
|
|
|
|
|
|
|
|
def noteNameToMidiPitch(s):
|
|
""" Convert a name ('e', 'g#') to a MIDI pitch. """
|
|
|
|
tb = { 'c': 0, 'c#': 1, 'd&': 1, 'd': 2, 'd#': 3, 'e&': 3,
|
|
'e': 4, 'f&': 4, 'e#': 5, 'f': 5, 'f#': 6, 'g&': 6,
|
|
'g': 7, 'g#': 8, 'a&': 8, 'a': 9, 'a#': 10, 'b&': 10,
|
|
'b': 11, 'b&': 11, 'c&': 11, 'b#': 0 }
|
|
|
|
# strip and count trailing '+' and '-'
|
|
|
|
if '-' in s and '+' in s:
|
|
return None
|
|
|
|
adjust = 0
|
|
while s.endswith('-'):
|
|
adjust -= 12
|
|
s=s[:-1]
|
|
while s.endswith('+'):
|
|
adjust += 12
|
|
s=s[:-1]
|
|
|
|
try:
|
|
value = tb[s] # puts into middle octave 60==5*12
|
|
except:
|
|
return None
|
|
|
|
return value + adjust
|
|
|
|
|
|
#not used. Leave for debugging??
|
|
def MidiPitch2NoteName(value):
|
|
nameLookUp = [ 'c','c#','d','e&','e','f','f#','g','g#','a','b&','b' ]
|
|
|