# HG changeset patch # User wim # Date 1590395958 -7200 # Node ID 7fd6cac1a69d822e478a1ffb11600f8e5e246fce # Parent 4896b49e870a85dacc611deecd8a3ab9abb745d4 - hard link to segLibB40 added diff -r 4896b49e870a -r 7fd6cac1a69d segLibB40.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)) diff -r 4896b49e870a -r 7fd6cac1a69d xml2b40.py --- /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] ') + 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