csong@0: ''' csong@0: Author: Chunyang Song csong@0: Institution: Centre for Digital Music, Queen Mary University of London csong@0: csong@0: ** Longuet-Higgins and Lee's Model (LHL) ** csong@0: csong@0: Algorithm: csong@0: csong@0: Only applicable to simple rhtyhms. csong@0: csong@0: Generate tree structure: csong@0: 1) Time_signature decides sequence of subdivisions: 4/4 - [2,2,2,...2], 3/4 - [3,2,2,...2], 6/8 - [2,3,2,2...2]; csong@0: 2) If after subdivision, a node has at least one branch that is full rest or one note, then it cannot be subdivided; csong@0: 3) Weight of node, initialized as 0, decrease by 1 with increasing depth of the tree. csong@0: csong@0: Assign each node in the tree three attributes - weight, pos, event; csong@0: Search for Note-Rest pairs that Note's weight is no more than Rest's weight; csong@0: Syncopation for such a pair is the difference in weights; csong@0: Total Syncopation value for non-syncopation rhythms is -1; for syncopated rhythms is the sum of syncopation of all notes. csong@0: csong@0: In order to evaluate LHL models with the others, re-scale LHL's measures to non-negative. csong@0: Apart from the immeasuable rhythms, add 1 to each syncopation value. csong@0: csong@0: ''' csong@0: from MeterStructure import MeterStructure csong@0: csong@0: class Node: csong@0: def __init__(self, pos, event): csong@0: self.pos = pos csong@0: self.event = event csong@0: csong@0: def assignWeight(self, time_sig): csong@0: ms = MeterStructure(time_sig) csong@0: # Retrieve the LHL meter hierarchy in one bar; csong@0: #(e.g. weight = [0, -4, -3, -4, -2, -4, -3, -4, -1, -4, -3, -4, -2, -4, -3, -4] in 4/4 meter) csong@0: weights = ms.getLHLWeights(1) csong@0: csong@0: # find the metrical weight of the note that locates at certain position(self.pos) csong@0: # index is the corresponding position of "self.pos" in the "weights" list csong@0: index = int(((abs(self.pos)%48)/48.0)*len(weights)) csong@0: self.weight = weights[index] csong@0: csong@0: class Tree: csong@0: def __init__(self): csong@0: self.nodes = [] csong@0: csong@0: def add(self, Node): csong@0: self.nodes.append (Node) csong@0: csong@0: def getWeight(self, time_sig): csong@0: for n in self.nodes: csong@0: n.assignWeight(time_sig) csong@0: csong@0: def display(self): csong@0: print 'Pos, Event, Weight' csong@0: for n in self.nodes: csong@0: print '%02d %s %02d' % (n.pos, n.event, n.weight) csong@0: csong@0: csong@0: def subdivide(sequence, segments_num): csong@0: subSeq = [] csong@0: if len(sequence) % segments_num != 0: csong@0: print 'Error: rhythm segment cannot be equally subdivided.' csong@0: else: csong@0: n = len(sequence) / segments_num csong@0: start , end = 0, n csong@0: for i in range(segments_num): csong@0: subSeq.append(sequence[start : end]) csong@0: start = end csong@0: end = end + n csong@0: csong@0: return subSeq csong@0: csong@0: csong@0: def eventType (sequence): csong@0: if not(1 in sequence): csong@0: event = 'R' # Full rest csong@0: elif sequence[0] == 1 and not(1 in sequence[1:]): csong@0: event = 'N' # Only one on-beat note csong@0: else: csong@0: event = 'D' # Divisable csong@0: csong@0: return event csong@0: csong@0: def splitInto2 (sequence, tree, pos, isRecursive): csong@0: csong@0: subs = subdivide(sequence, 2) csong@0: csong@0: for s in subs: csong@0: if eventType(s) == 'R' or eventType(s) == 'N': csong@0: #print 'test', s, eventType(s), pos csong@0: tree.add( Node(pos, eventType(s)) ) csong@0: else: csong@0: if isRecursive: csong@0: #print 'test', s, eventType(s), pos csong@0: splitInto2 (s, tree, pos, True) csong@0: else: csong@0: splitInto3 (s, tree, pos) csong@0: csong@0: pos = pos + len(sequence) / 2 csong@0: csong@0: return tree csong@0: csong@0: def splitInto3 (sequence, tree, pos): csong@0: csong@0: subs = subdivide(sequence, 3) csong@0: csong@0: for s in subs: csong@0: if eventType(s) == 'R' or eventType(s) == 'N': csong@0: #print 'test', s, eventType(s), pos csong@0: tree.add( Node(pos, eventType(s)) ) csong@0: csong@0: else: csong@0: splitInto2 (s, tree, pos, True) csong@0: csong@0: pos = pos + len(sequence) / 3 csong@0: csong@0: return tree csong@0: csong@0: csong@0: def createTree(rhythm, time_sig, bar): csong@0: t = Tree() csong@0: # The root is the rhtyhm in a entire bar, has weight 0, at position 0 csong@0: weight, pos, event = 0 , 0, eventType(rhythm) csong@0: root = Node (pos, event) csong@0: if '2/4' in time_sig: csong@0: t.add ( Node(-24, 'N') ) # Treat the last metronome beat in the previous bar as a sounded note csong@0: csong@0: if event == 'D': csong@0: t = splitInto2(rhythm, t, pos, True) csong@0: else: csong@0: t.add (root) csong@0: csong@0: elif '4/4' in time_sig: csong@0: t.add ( Node(-12, 'N') ) # Treat the last metronome beat in the previous bar as a sounded note csong@0: csong@0: if event == 'D': csong@0: t = splitInto2(rhythm, t, pos, True) csong@0: else: csong@0: t.add (root) csong@0: csong@0: elif '3/4' in time_sig: csong@0: t.add ( Node(-8, 'N') ) # Treat the last metronome beat in the previous bar as a sounded note csong@0: csong@0: segments = subdivide (rhythm, bar) csong@0: csong@0: for s in segments: csong@0: if eventType(s) == 'D': csong@0: t = splitInto3(s, t, pos) csong@0: pos = pos + len(rhythm)/bar csong@0: csong@0: csong@0: elif '6/8' in time_sig: csong@0: t.add ( Node(-8, 'N') ) # Treat the last metronome beat in the previous bar as a sounded note csong@0: csong@0: segments = subdivide (rhythm, bar) csong@0: csong@0: for s in segments: csong@0: if eventType(s) == 'D': csong@0: t = splitInto2(s, t, pos, False) csong@0: pos = pos + len(rhythm)/bar csong@0: csong@0: else: csong@0: print 'This time signature is not defined. Choose between 4/4, 3/4 or 6/8' csong@0: csong@0: return t csong@0: csong@0: csong@0: def lhl(rhythm, time_sig, category, bar): csong@0: measures = [] csong@0: csong@0: if 'poly' in category: csong@0: return -1 csong@0: else: csong@0: tree = createTree(rhythm, time_sig, bar) csong@0: tree.getWeight(time_sig) csong@0: #tree.display() csong@0: csong@0: # find NR-pair that N's weight <= R's weight, add difference in weight to measures[] csong@0: N_weight = tree.nodes[0].weight csong@0: size = len(tree.nodes) csong@0: csong@0: for i in range(1, size): csong@0: csong@0: if tree.nodes[i].event == 'N': csong@0: N_weight = tree.nodes[i].weight csong@0: csong@0: if tree.nodes[i].event == 'R' and tree.nodes[i].weight >= N_weight: csong@0: measures.append(tree.nodes[i].weight - N_weight ) csong@0: csong@0: # Calculate syncopation csong@0: if len(measures) == 0: # syncopation of non-syncopated rhythm is -1 csong@0: syncopation = -1 csong@0: else: csong@0: syncopation = sum(measures) # syncopation of syncopated rhythm is the sum of measures[] csong@0: csong@0: return syncopation + 1 # For evaluation purpose, scale up by 1 to make scores above 0 csong@0: csong@0: csong@0: # Retrieve the stimuli csong@0: #f = file('stimuli.txt') csong@0: f = file('stimuli_34only.txt') csong@0: csong@0: #Calculate syncopation for each rhythm pattern csong@0: while True: csong@0: line = f.readline().split(';') csong@0: if len(line) == 1: csong@0: break csong@0: else: csong@0: sti_name = line[0] csong@0: rhythmString = line[1].split() csong@0: time_sig = line[2] csong@0: category = line[3] csong@0: bar = int(line[4]) csong@0: csong@0: rhythm = map(int,rhythmString[0].split(',')) csong@0: csong@0: print sti_name, lhl(rhythm, time_sig, category, bar)