- hard link to segLibB40 added
authorwim
Mon, 25 May 2020 10:39:18 +0200
changeset 1 7fd6cac1a69d
parent 0 4896b49e870a
child 2 775a1d0d3be6
- hard link to segLibB40 added
segLibB40.py
xml2b40.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/segLibB40.py	Mon May 25 10:39:18 2020 +0200
@@ -0,0 +1,217 @@
+'''B40 versie
+bevat B40lib, b40ana en b40krumm
+zie ook b40krumm.py voor uitleg van b40 keyalgoritme.
+'''
+import re, math
+
+major = [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88]
+minor = [6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17]
+base_templates = [(0,12,23),(0,12,23,34),(0,11,23),(0,11,22,33),(0,11,22,34),(0,11,22)]
+base_names = ['maj','dom7','min','fdim','hdim','dim']
+
+def avg (xs):
+    return sum (xs) / len (xs)
+
+tab = [1,10,11,0,1,2,3,0,1,2,3,4,1,2,3,4,5,6,3,4,5,6,7,4,5,6,7,8,9,6,7,8,9,10,11,8,9,10,11,0]
+acs = [2,-2,-1,0,1,2,0,-2,-1,0,1,2,0,-2,-1,0,1,2,-2,-1,0,1,2,0,-2,-1,0,1,2,0,-2,-1,0,1,2,0,-2,-1,0,1]
+def b40pcHst (b40Hst):
+    pcHst = [0.0 for i in range (12)]
+    nacc = 0
+    for ix, b40 in enumerate (b40Hst):
+        if b40 == 0: continue
+        pcHst [tab [ix]] += float (b40) # soms valt een Dbb op een C b.v. ex14, maat 5, 3e tel
+        nacc += acs [ix] * b40
+    return pcHst, nacc
+
+# de b40 nummers van de 12 pitches [3,[4,8],9,[10,14],15,20,[21,25],26,[27,31],32,[33,37],38]
+b12b40 = [3,4,9,10,15,20,21,26,27,32,33,38]
+dkeus = {}.fromkeys ([1,3,6,8,10], 1)
+
+def keyCor (b40Hst):
+    pcHst, nacc = b40pcHst (b40Hst)
+    pcHst [0] += 0.0001
+    pc_mean = avg (pcHst)
+    maj_mean = avg (major)
+    min_mean = avg (minor)
+    maj_dif_sq = sum ([(m - maj_mean)**2 for m in major])
+    min_dif_sq = sum ([(m - min_mean)**2 for m in minor])
+    pc_dif_sq  = sum ([(m -  pc_mean)**2 for m in pcHst])
+    maj_noemer = math.sqrt (maj_dif_sq * pc_dif_sq)
+    min_noemer = math.sqrt (min_dif_sq * pc_dif_sq)
+
+    cor_maj = []; cor_min = []
+    for i in range (12):
+        maj_cor = min_cor = 0
+        for j in range (12):
+            k = (i + j) % 12
+            maj_cor += (major[j] - maj_mean) * (pcHst[k] - pc_mean)
+            min_cor += (minor[j] - min_mean) * (pcHst[k] - pc_mean)
+        cor_maj.append (maj_cor / maj_noemer)
+        cor_min.append (min_cor / min_noemer)
+
+    ks = [(c,i,0) for i,c in enumerate (cor_maj)] + [(c,i,1) for i,c in enumerate (cor_min)]
+    ks.sort ()
+    ks.reverse ()
+    ksb40 = []
+    for c,b12,mnr in ks:
+        b40 = b12b40 [b12]
+        if b12 in dkeus and nacc < 0: # er zijn meer mollen dan kruisen
+            b40 += 4                  # C# => Db (4 => 8) etc.
+        ksb40.append ((c, b40, mnr))
+    return ksb40
+
+# the note names on the line of fifth in order of sharpness. The sharpness of the notes is:
+# -13 (Fbb), -12, -11, ... 0 (Bb), 1, 2 (=C) , ... 21 (B##)
+Lof = ['Fbb','Cbb','Gbb','Dbb','Abb','Ebb','Bbb','Fb','Cb','Gb','Db','Ab','Eb','Bb',
+       'F','C','G','D','A','E','B','F#','C#','G#','D#','A#','E#','B#',
+       'F##','C##','G##','D##','A##','E##','B##']
+       
+# returns the base40 number of a note (between 1 and 40) given the sharpness (between -13 and 21)
+def loftobase40 (lof):
+    return (lof + 12) * 23 % 40 + 1
+
+# returns the sharpness of a note given the base40 number
+def base40tolof (b40):
+    return (b40 * 7 - 1) % 40 - 18
+
+# returns the name of a b40 note number
+def b40nm (b40):
+    if b40 in [6,12,23,29,35]: name = '' # no note on these positions
+    else:
+        ix = base40tolof (b40) # the sharpness
+        name = Lof [ix+13]     # +13 -> index in the line of fifth table
+    return name
+
+rc = re.compile (r'([-A-GX][b#]*)(.*)')
+def splitCh (chnm):
+    ro = rc.match (chnm)
+    if not ro: raise '%s matcht niet' % chnm
+    return ro.group (1), ro.group (2)
+
+def mkTemplates ():
+    global templates, accNames, freqTab
+    templates = []
+    accNames = []
+    freqTab = [] # template index -> frequentie index = (5..0), 5 meest voorkomend type, 0 minst voorkomend
+    for sh in range (-6,15):   # sharpness of 'Fb','Cb', Gb, ... F, C, G, ... D#, A#, E#, B#
+        b40 = loftobase40 (sh) # base40 number = roots of chords
+        for it, t in enumerate (base_templates):
+            tx = tuple( [(ival + b40) % 40 for ival in t] )      # the b40 notes of the chord
+            ex = tuple( [n for n in range (40) if not n in tx] ) # the notes notes not in the chord
+            templates.append ((tx, ex))
+            accNames.append (b40nm (b40) + base_names[it])
+    for acc in accNames:
+        chroot, type = splitCh (acc)
+        freqIx = len (base_names) - base_names.index (type) # hoogste index voor meestvoorkomende chordtype
+        freqTab.append (freqIx)
+
+def score (iseg, jseg):
+    w = getSegment (iseg, jseg)
+    scores = []
+    nseg = jseg - iseg + 1
+    for it, (tx, ex) in enumerate (templates):
+        notes = [w[i] for i in tx]
+        fout = [w[i] for i in ex]
+        mis = sum ([1 for n in notes if n == 0])
+        s = sum (notes) - sum (fout) - mis
+        #~ ps = [n for n in notes if n > 0]  # noot histogram van segment
+        #~ if ps and min (ps) < nseg * 0.3: s -= 2 # niet gewogen (oud), bevoordeelt s01 t.o.v. s0 + s1
+        #~ if ps and min (ps) * 4 < nseg: s -= int (round (0.1 * nseg)) # nieuw, gewogen
+        rootw = notes[0] # root weight voor tie-break
+        acc = accNames [it]
+        freqIx = freqTab [it]
+        scores.append ((s, rootw, freqIx, acc))
+    scores.sort ()
+    scores.reverse () # hoogste score, hoogste root-weight, hoogste freqIx komt bovenaan
+    highest, _, _, acc = scores[0]
+    return highest, acc, scores[:5] # score, acc-name
+
+def readEvents (events, resolution=2): # list of [time, +/- midi note number]
+    #~ events.sort ()
+    tgroep = events[0][0]
+    groep = []
+    merged = []
+    for t, p in events:
+        if t - tgroep < resolution:
+            groep.append (p)
+            tgroep = t
+        else:
+            merged.append ((tgroep, groep))
+            tgroep = t
+            groep = [p]
+    merged.append ((tgroep, groep))
+    klinkt = []
+    kgroep = []
+    for t, g in merged:
+        for p in g:
+            if p > 0: klinkt.append (p)
+            elif -p in klinkt: klinkt.remove (-p)
+            else: print 'unmatched off-message'
+        kgroep.append ((t, klinkt[:]))
+    return kgroep
+
+def mkWeights (kgroep):
+    global weights
+    weights = [[] for i in range (len (kgroep))]
+    for ix, (t, g) in enumerate (kgroep):
+        w = 40 * [0]
+        for n in g: w [n % 40] += 1
+        weights [ix] = w
+    return weights
+
+def getSegment (i, j):
+    wtot = 40 * [0]
+    for ws in weights [i:j+1]:
+        for i, w in enumerate (ws):
+            wtot [i] += w
+    return wtot
+
+def analyseK (kgroep, debug=0):
+    if debug: print 'aantal segmenten', len (kgroep)
+
+    #~ mkWeights (kgroep)
+    #~ mkTemplates ()
+
+    j0, j1, j2 = 0,1,2
+    s0, acc0, xs0 = score (j0, j0)
+    s1, acc1, xs1 = score (j1, j1)
+    s01, acc01, xs01 = score (j0, j1)
+    segs = []
+    while j1 < len (weights):
+        s2, acc2, xs2 = score (j2, j2)
+        s12, acc12, xs12 = score (j1, j2)
+        s012, acc012, xs012 = score (j0, j2)
+        left = max ([s0+s12, s0+s1+s2])
+        right = max ([s012, s01+s2])
+        if left <= right:
+            j1, j2 = j2, j2 + 1
+            s0, acc0, xs0 = s01, acc01, xs01
+            s01, acc01, xs01 = s012, acc012, xs012
+        else:
+            segs.append ((j0,j1,s0,acc0,xs0))
+            if debug: print j0, j1, s0, acc0
+            j0, j1, j2 = j1, j2, j2 + 1
+            s0, acc0, xs0 = s1, acc1, xs1
+            s01, acc01, xs01 = s12, acc12, xs12
+        s1, acc1, xs1 = s2, acc2, xs2
+    if j0 < len (weights):
+        segs.append ((j0,j1,s0,acc0,xs0))
+        if debug: print j0, j1, s0, acc0
+
+    return segs, kgroep
+
+if __name__ == '__main__':
+    f = open ('mids/bwv539p.b40')
+    xs = f.readlines ()
+    f.close ()
+    def toint (xs): return map (lambda x: int (x), xs)
+    events = map (lambda x: toint (x.strip().split()), xs)
+    kgroep = readEvents (events)
+    mkWeights (kgroep)
+    mkTemplates ()
+    segs, kgroep = analyseK (kgroep, debug=0)
+    print '%d segs, %d groepen' % (len (segs), len (kgroep))
+    for iseg, jseg, score, acc, rest in segs:
+        print '---', acc, score
+        for t, ns in kgroep [iseg:jseg]:
+            print t, ', '.join (map (b40nm, ns))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/xml2b40.py	Mon May 25 10:39:18 2020 +0200
@@ -0,0 +1,189 @@
+#!/usr/bin/env python
+# coding: latin-1
+''' Maakt een .b40 file uit een .xml file (MusicXML).
+muziek = [parts], part = [maten], maat = [voices]
+voice = [[dur, tijd, noot1, noot2, ....]]
+per part: len (voices) = maxvoice
+vceCnt telt het aantal stemmen in het hele stuk (d.w.z. over meerdere parts)
+In deze versie worden eerst alle noten in één part archter elkaar gezet en dan pas worden
+gebonden noten samengevoegd. Dit in verband met het terugzetten van de tijd door backup, waardoor
+eem gebonden noot tussen twee stemmen verloren gaat als voor de backup (naar de andere stem)
+de noot nog een keer voorkomt.
+'''
+import operator, sys, os
+import xml.etree.ElementTree as E
+
+b40List = ['Cbb','Cb','C','C#','C##','-','Dbb','Db','D','D#','D##','-','Ebb','Eb','E','E#','E##','Fbb','Fb','F',
+           'F#','F##','-','Gbb','Gb','G','G#','G##','-','Abb','Ab','A','A#','A##','-','Bbb','Bb','B','B#','B##']
+b40Dict = dict (zip (b40List, range (len (b40List))))
+
+gtikkenPerKwart = 384   # ticks per quarter note
+gC5 = False             # central C == C5 if gC5 else C4
+
+def ntB40 (p, o, alt):
+    if alt:
+        alt = int (alt)
+        while alt > 0: p += '#'; alt -= 1
+        while alt < 0: p += 'b'; alt += 1
+    b40 = o * 40 + b40Dict[p] + 1
+    return str (b40)
+
+def doNote (n):
+    global tijd, maxVoice
+    chord = n.find ('chord') != None
+    p = n.findtext ('pitch/step')
+    alt = n.findtext ('pitch/alter')
+    o = n.findtext ('pitch/octave')
+    r = n.find ('rest')
+    dur = int (n.findtext ('duration'))
+    v = int (n.findtext ('voice')) - 1
+    if v > maxVoice: maxVoice = v
+    if r != None: noot = 'z'
+    else: noot = ntB40 (p, int (o) + (1 if gC5 else 0), alt)
+    tie = n.find ('tie')
+    if tie != None and tie.get ('type') == 'stop': noot = '-' + noot
+    if chord: addChord (v, noot)
+    else:     appendNote (v, dur, noot)
+
+def doAttr (e):
+    global durUnit
+    kmaj = ['Cb','Gb','Dd','Ab','Eb','Bb','F','C','G','D','A', 'E', 'B', 'F#','C#']
+    kmin = ['Ab','Eb','Bb','F', 'C', 'G', 'D','A','E','B','F#','C#','G#','D#','A#']
+    dvstxt = e.findtext ('divisions')
+    if dvstxt: # niet alle attribuut knopen hebben een divisions (b.v. key change)
+        #~ print 'divisions per kwart', int (dvstxt)
+        durUnit = int (dvstxt)
+    key = ''
+    f = e.findtext ('key/fifths')
+    m = e.findtext ('key/mode')
+    if m == 'major': key = kmaj [7 + int (f)]
+    if m == 'minor': key = kmin [7 + int (f)] + 'min'
+    if key:
+        for v in range (nVoices): appendNote (v, 0, 'K:%s\n' % key)
+    beats = e.findtext ('time/beats')
+    if beats:
+        met = beats + '/' + e.findtext ('time/beat-type')
+        for v in range (nVoices): appendNote (v, 0, 'M:%s\n' % met)
+
+def appendNote (v, dur, noot):
+    global tijd
+    t = vtimes [v]
+    if tijd > t: voices [v].append ([tijd - t, t, 'z'])
+    if tijd < t: raise 'kenniet'
+    voices [v].append ([dur, tijd, noot])
+    tijd += int (dur)
+    vtimes[v] = tijd
+
+def addChord (v, noot):
+    t = voices[v][-1]
+    voices[v][-1] = t + [noot]
+
+def addBar ():
+    global voices
+    for v in range (nVoices): appendNote (v, 0, ' |\n')
+    gMaten.append (voices)
+    voices = [[] for i in range (nVoices)]
+
+def outMaat (i, mbuf, ns):
+    for nx in mbuf:
+        dur = nx[0]  * gTikkenPerKwart / durUnit
+        tijd = nx[1] * gTikkenPerKwart / durUnit
+        if not dur: continue # alleen echte noten
+        for noot in nx[2:]: # de noot of noten in chord
+            if 'z' in noot: continue # skip rusten
+            ns.append ((tijd, noot, dur))
+
+def remTies (ns):
+    global noten
+    tieBufDur = {}  # tijdelijk vasthouden van noten voor evt. volgende tie
+    tieBufTijd = {} # twee aparte buffers voor duur en tijd
+    ns.sort ()
+    for tijd, noot, dur in ns: # nu nog voor gebonden noten checken
+        if noot.startswith ('-'): tieBufDur [noot[1:]] += dur
+        else:
+            if noot in tieBufDur: # bij de volgdende niet gebonden noot vorige uitvoeren
+                noten.append ((tieBufTijd [noot], noot, tieBufDur[noot]))
+            tieBufTijd [noot] = tijd # alle noten vast houden voor evt. volgende tie
+            tieBufDur [noot] = dur
+    for noot, dur in tieBufDur.iteritems (): # alle vastgehouden noten nu uitvoeren
+        noten.append ((tieBufTijd[noot], noot, dur))
+
+def outVoices (vceCnt):         # output alle stemmen in een part, nummering begint bij vceCnt
+    global tijd, gMaten, voices, vtimes, tupcnt, maxVoice, noten
+    ns = []
+    for maat in gMaten: # aantal gebruikte voices in gMaten = maxVoice + 1
+        mbuf = reduce (operator.concat, maat[:maxVoice + 1]) # plak de gebruikte voices aanelkaar
+        outMaat (vceCnt, mbuf, ns)    # alle stemmen in deze maat => ns, en nummer telkens vanaf vceCnt
+    remTies (ns)
+    vceCnt += maxVoice + 1 # de volgende part start met aansluitend hogere voice nummers
+    gMaten = []
+    voices = [[] for i in range (nVoices)]
+    vtimes = nVoices * [0]
+    tijd = 0
+    tupcnt = 0
+    maxVoice = 0
+    return vceCnt
+
+def xml2b40 (fnmext, tpk=384, C5=1): # base name, ticks per quarter, central C == C5
+    global nVoices, gMaten, voices, vtimes, tijd, tupcnt, maxVoice, vceCnt, noten, gTikkenPerKwart, gC5
+    gTikkenPerKwart = tpk
+    gC5 = C5
+    nVoices = 10
+    gMaten = []
+    voices = [[] for i in range (nVoices)]
+    vtimes = nVoices * [0]
+    tijd = 0
+    tupcnt = 0
+    maxVoice = 0
+    vceCnt = 1
+    noten = []
+
+    e = E.parse (fnmext)
+    parts = e.findall ('part')
+    noten = []      # de uitvoer van alle noten
+    for p in parts:
+        maten = p.findall ('measure')
+        for maat in maten:
+            es = maat.getchildren ()
+            for e in es:
+                if e.tag == 'note': doNote (e)
+                elif e.tag == 'attributes': doAttr (e)
+                elif e.tag == 'backup':
+                    dt = int (e.findtext ('duration'))
+                    tijd -= dt
+                elif e.tag == 'forward':
+                    dt = int (e.findtext ('duration'))
+                    tijd += dt
+                else: pass
+            addBar ()
+        vceCnt = outVoices (vceCnt)
+    offnoten = []
+    for tijd, noot, dur in noten:
+        noot = int (noot) # straks numeriek ordenen i.v.m. vergelijken oude b40 files
+        offnoten.append ((tijd, noot))
+        offnoten.append ((tijd + dur, -noot))
+    noten = offnoten
+    noten.sort ()
+    return noten
+
+
+if __name__ == '__main__':
+    from optparse import OptionParser
+    parser = OptionParser (usage='%prog [-h] [-g TPQ] [--C5] <file1>')
+    parser.add_option ("-t", action="store", type="int", help="ticks per quarternote", default=120, metavar='TPQ')
+    parser.add_option ("--C5", action="store_true", help="central C is C5", default=False)
+    parser.add_option ("-w", action="store_true", help="write .b40 file", default=False)
+    options, args = parser.parse_args ()
+    if len (args) < 1: parser.error ('file argument needed')
+    fnmext = args [0]
+    fnm, ext = os.path.splitext (fnmext)
+    if ext != '.xml': parser.error ('.xml file needed file needed')
+    if not os.path.exists (fnmext): parser.error ('%s does not exist' % fnmext)
+    xml2b40 (fnmext, options.t, options.C5)
+    if options.w:
+        g = file (fnm + '.b40', 'w')
+        for tijd, noot in noten: g.write ('%d %s\n' % (tijd, noot))
+        g.close ()
+        print fnm + '.b40 written'
+    else:
+        for tijd, noot in noten: print tijd, noot