mirror of
https://github.com/microtherion/VocalEasel.git
synced 2024-12-22 03:04:00 +00:00
422 lines
13 KiB
Python
422 lines
13 KiB
Python
|
|
# patChord.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>
|
|
|
|
"""
|
|
|
|
|
|
import random
|
|
|
|
import MMA.notelen
|
|
|
|
import gbl
|
|
from MMA.common import *
|
|
from MMA.pat import PC
|
|
import copy
|
|
|
|
|
|
class Voicing:
|
|
def __init__(self):
|
|
self.mode = None
|
|
self.range = 12
|
|
self.center = 4
|
|
self.random = 0
|
|
self.percent = 0
|
|
self.bcount = 0
|
|
self.dir = 0
|
|
|
|
|
|
class Chord(PC):
|
|
""" Pattern class for a chord track. """
|
|
|
|
vtype = 'CHORD'
|
|
sortDirection = 0 # used for tracking direction of strummed chords
|
|
|
|
def __init__(self, ln):
|
|
self.voicing = Voicing()
|
|
PC.__init__(self, ln)
|
|
|
|
def saveGroove(self, gname):
|
|
""" Save special/local variables for groove. """
|
|
|
|
PC.saveGroove(self, gname) # create storage. Do this 1st.
|
|
self.grooves[gname]['VMODE'] = copy.deepcopy(self.voicing)
|
|
|
|
def restoreGroove(self, gname):
|
|
""" Restore special/local/variables for groove. """
|
|
|
|
self.voicing = self.grooves[gname]['VMODE']
|
|
PC.restoreGroove(self, gname)
|
|
|
|
|
|
def clearSequence(self):
|
|
""" Set some initial values. Called from init and clear seq. """
|
|
|
|
PC.clearSequence(self)
|
|
self.voicing = Voicing()
|
|
# .direction was set in PC.clear.. we're changing to our default
|
|
self.direction = seqBump(['UP'])
|
|
|
|
|
|
def setVoicing(self, ln):
|
|
""" set the Voicing Mode options. Only valid for CHORDS. """
|
|
|
|
notopt, ln = opt2pair(ln, toupper=1)
|
|
|
|
if notopt:
|
|
error("Voicing: Each Voicing option must be a OPT=VALUE pair.")
|
|
|
|
for mode, val in ln:
|
|
if mode == 'MODE':
|
|
valid= ("-", "OPTIMAL", "NONE", "ROOT", "COMPRESSED", "INVERT")
|
|
|
|
if not val in valid:
|
|
error("Valid Voicing Modes are: %s" % " ".join(valid))
|
|
|
|
if val in ('-', 'NONE',"ROOT"):
|
|
val = None
|
|
|
|
|
|
if val and (max(self.invert) + max(self.compress)):
|
|
warning("Setting both VoicingMode and Invert/Compress is not a good idea")
|
|
|
|
""" When we set voicing mode we always reset this. This forces
|
|
the voicingmode code to restart its rotations.
|
|
"""
|
|
|
|
self.lastChord = []
|
|
|
|
self.voicing.mode = val
|
|
|
|
|
|
elif mode == 'RANGE':
|
|
val = stoi(val, "VOICING RANGE %s: Arg must be a value" % self.name)
|
|
|
|
if val < 1 or val > 30:
|
|
error("Voicing Range: Arg out-of-range; must be 1 to 30, not '%s'." % val)
|
|
|
|
self.voicing.range = val
|
|
|
|
|
|
elif mode == 'CENTER':
|
|
val = stoi(val, "Argument for %s VOICING CENTER must be a value" % self.name)
|
|
|
|
if val < 1 or val > 12:
|
|
error("VOICING CENTER: arg out-of-range; must be 1 to 12, not '%s'." % val)
|
|
|
|
self.voicing.center = val
|
|
|
|
elif mode == 'RMOVE':
|
|
val = stoi(val, "Argument for %s VOICING RANDOM must be a value" % self.name)
|
|
|
|
if val < 0 or val > 100:
|
|
error("VOICING RANDOM: arg must be 0 to 100, not %s" % val)
|
|
|
|
self.voicing.random = val
|
|
self.voicing.bcount = 0
|
|
|
|
elif mode == 'MOVE':
|
|
val = stoi(val, "Argument for %s VOICING MOVE must be a value" % self.name)
|
|
|
|
if val < 0 :
|
|
error("VOICING MOVE: bar count must >= 0, not %s" % val)
|
|
if val > 20:
|
|
warning("VOICING MOVE: bar count '%s' is quite large" % val)
|
|
|
|
self.voicing.bcount = val
|
|
self.voicing.random = 0
|
|
|
|
elif mode == 'DIR':
|
|
val = stoi(val, "Argument for %s VOICING DIR must be a value" % self.name)
|
|
|
|
if not val in (1,0,-1):
|
|
error("VOICING MOVE: Dir must be -1, 0 or 1, not '%s'." % val)
|
|
|
|
self.voicing.dir = val
|
|
|
|
|
|
if gbl.debug:
|
|
v=self.voicing
|
|
print "Set %s Voicing MODE=%s" % (self.name, v.mode),
|
|
print "RANGE=%s CENTER=%s" % (v.range, v.center),
|
|
print "RMOVE=%s MOVE=%s DIR=%s" % (v.random, v.bcount, v.dir)
|
|
|
|
|
|
def setDupRoot(self, ln):
|
|
""" set/unset root duplication. Only for CHORDs """
|
|
|
|
|
|
ln = lnExpand(ln, '%s DupRoot' % self.name)
|
|
tmp = []
|
|
|
|
for n in ln:
|
|
n = stoi(n, "Argument for %s DupRoot must be a value" % self.name)
|
|
|
|
if n < -9 or n > 9:
|
|
error("DupRoot %s out-of-range; must be -9 to 9" % n)
|
|
|
|
tmp.append( n * 12 )
|
|
|
|
self.dupRoot = seqBump(tmp)
|
|
|
|
if gbl.debug:
|
|
print "Set %s DupRoot to " % self.name,
|
|
printList(ln)
|
|
|
|
|
|
def getPgroup(self, ev):
|
|
""" Get group for chord pattern.
|
|
|
|
Tuples: [start, length, volume (,volume ...) ]
|
|
"""
|
|
|
|
if len(ev) < 3:
|
|
error("There must be at least 3 items in each group "
|
|
"of a chord pattern definition, not <%s>" % ' '.join(ev))
|
|
|
|
a = struct()
|
|
|
|
a.offset = self.setBarOffset(ev[0])
|
|
a.duration = MMA.notelen.getNoteLen(ev[1])
|
|
|
|
vv = ev[2:]
|
|
if len(vv)>8:
|
|
error("Only 8 volumes are permitted in Chord definition, not %s" % len(vv))
|
|
|
|
a.vol = [0] * 8
|
|
for i,v in enumerate(vv):
|
|
v=stoi(v, "Expecting integer in volume list for Chord definition")
|
|
a.vol[i]=v
|
|
|
|
for i in range(i+1,8): # force remaining volumes
|
|
a.vol[i]=v
|
|
|
|
return a
|
|
|
|
def restart(self):
|
|
self.ssvoice = -1
|
|
self.lastChord = None
|
|
|
|
|
|
def chordVoicing(self, chord, vMove):
|
|
""" Voicing algorithm by Alain Brenzikofer. """
|
|
|
|
|
|
sc = self.seq
|
|
vmode=self.voicing.mode
|
|
|
|
if vmode == "OPTIMAL":
|
|
|
|
# Initialize with a voicing around centerNote
|
|
|
|
chord.center1(self.lastChord)
|
|
|
|
# Adjust range and center
|
|
|
|
if not (self.voicing.bcount or self.voicing.random):
|
|
chord.center2(self.voicing.center, self.voicing.range/2)
|
|
|
|
|
|
# Move voicing
|
|
|
|
elif self.lastChord:
|
|
if (self.lastChord != chord.noteList ) and vMove:
|
|
chord.center2(self.voicing.center,self.voicing.range/2)
|
|
vMove = 0
|
|
|
|
# Update voicingCenter
|
|
|
|
sum=0
|
|
for n in chord.noteList:
|
|
sum += n
|
|
c=sum/chord.noteListLen
|
|
|
|
""" If using random voicing move it it's possible to
|
|
get way off the selected octave. This check ensures
|
|
that the centerpoint stays in a tight range.
|
|
Note that if using voicingMove manually (not random)
|
|
it is quite possible to move the chord centers to very
|
|
low or high keyboard positions!
|
|
"""
|
|
|
|
if self.voicing.random:
|
|
if c < -4: c=0
|
|
elif c >4: c=4
|
|
self.voicing.center=c
|
|
|
|
|
|
elif vmode == "COMPRESSED":
|
|
chord.compress()
|
|
|
|
elif vmode == "INVERT":
|
|
if chord.rootNote < -2:
|
|
chord.invert(1)
|
|
|
|
elif chord.rootNote > 2:
|
|
chord.invert(-1)
|
|
chord.compress()
|
|
|
|
self.lastChord = chord.noteList[:]
|
|
|
|
return vMove
|
|
|
|
|
|
def trackBar(self, pattern, ctable):
|
|
""" Do a chord bar. Called from self.bar() """
|
|
|
|
sc = self.seq
|
|
unify = self.unify[sc]
|
|
|
|
""" Set voicing move ONCE at the top of each bar.
|
|
The voicing code resets vmove to 0 the first
|
|
time it's used. That way only one movement is
|
|
done in a bar.
|
|
"""
|
|
|
|
vmove = 0
|
|
|
|
if self.voicing.random:
|
|
if random.randrange(100) <= self.voicing.random:
|
|
vmove = random.choice((-1,1))
|
|
elif self.voicing.bcount and self.voicing.dir:
|
|
vmove = self.voicing.dir
|
|
|
|
|
|
for p in pattern:
|
|
tb = self.getChordInPos(p.offset, ctable)
|
|
|
|
if tb.chordZ:
|
|
continue
|
|
|
|
self.crDupRoot = self.dupRoot[sc]
|
|
|
|
vmode = self.voicing.mode
|
|
vols = p.vol[0:tb.chord.noteListLen]
|
|
|
|
# Limit the chord notes. This works even if THERE IS A VOICINGMODE!
|
|
|
|
if self.chordLimit:
|
|
tb.chord.limit(self.chordLimit)
|
|
|
|
""" Compress chord into single octave if 'compress' is set
|
|
We do it here, before octave, transpose and invert!
|
|
Ignored if we have a VOICINGMODE.
|
|
"""
|
|
|
|
if self.compress[sc] and not vmode:
|
|
tb.chord.compress()
|
|
|
|
# Do the voicing stuff.
|
|
|
|
if vmode:
|
|
vmove=self.chordVoicing(tb.chord, vmove)
|
|
|
|
# Invert.
|
|
|
|
if self.invert[sc]:
|
|
tb.chord.invert(self.invert[sc])
|
|
|
|
""" Voicing adjustment for 'jazz' or altered chords. If a chord (most
|
|
likely something like a M7 or flat-9 ends up with any 2 adjacent
|
|
notes separated by a single tone an unconfortable dissonance results.
|
|
This little check compares all notes in the chord and will cut the
|
|
volume of one note to reduce the disonance. Usually this will be
|
|
the root note volume being decreased.
|
|
"""
|
|
|
|
nl=tb.chord.noteList
|
|
l=len(nl)
|
|
for j in range(l-1):
|
|
r = nl[j]
|
|
for i in range(j+1, l):
|
|
if nl[i] in (r-1, r+1, r-13, r+13) and vols[i] >= vols[0]:
|
|
vols[j] = vols[i]/2
|
|
break
|
|
|
|
loo = zip(nl, vols) # this is a note/volume array of tuples
|
|
|
|
|
|
""" Duplicate the root. This can be set from a DupRoot command
|
|
or by chordVoicing(). Notes:
|
|
- The volume for the added root will be the average of the chord
|
|
notes (ignoring OFF notes) divided by 2.
|
|
- If the new note (after transpose and octave adjustments
|
|
is out of MIDI range it will be ignored.
|
|
"""
|
|
|
|
if self.crDupRoot:
|
|
root = tb.chord.rootNote + self.crDupRoot
|
|
t = root + self.octave[sc] + gbl.transpose
|
|
if t >=0 and t < 128:
|
|
v=0
|
|
c=0
|
|
for vv in vols:
|
|
if vv:
|
|
v += vv
|
|
c += 2
|
|
v /= c
|
|
loo.append( (tb.chord.rootNote + self.crDupRoot, v))
|
|
|
|
# For strum we need to know the direction. Note that the direction
|
|
# is saved for the next loop (needed for alternating in BOTH).
|
|
|
|
sd = self.direction[sc]
|
|
if sd == 'BOTH':
|
|
if self.sortDirection:
|
|
self.sortDirection = 0
|
|
else:
|
|
self.sortDirection = 1
|
|
elif sd == 'DOWN':
|
|
self.sortDirection = 1
|
|
elif sd == 'RANDOM':
|
|
self.sortDirection = random.randint(0,1)
|
|
else:
|
|
self.sortDirection = 0
|
|
|
|
# take the list of notes and sort them in low to high order.
|
|
# reverse the list if direction is set.
|
|
|
|
loo.sort()
|
|
if self.sortDirection:
|
|
loo.reverse()
|
|
|
|
strumOffset = 0
|
|
|
|
for note, v in loo: # sorting low-to-high notes. Mainly for STRUM.
|
|
self.sendNote(
|
|
p.offset + strumOffset,
|
|
self.getDur(p.duration),
|
|
self.adjustNote(note),
|
|
self.adjustVolume(v, p.offset) )
|
|
|
|
strumOffset += self.getStrum(sc)
|
|
|
|
tb.chord.reset() # important, other tracks chord object
|
|
|
|
# Adjust the voicingMove counter at the end of the bar
|
|
|
|
if self.voicing.bcount:
|
|
self.voicing.bcount -= 1
|
|
|
|
|
|
|