diff Syncopation models/LHL.py @ 1:b2da092dc2e0

The consolidated syncopation software. Have finished individual model and basic functions. Need to revise the coding in main.py, and add rhythm-input interface.
author Chunyang Song <csong@eecs.qmul.ac.uk>
date Sun, 05 Oct 2014 21:52:41 +0100
parents 76ce27beba95
children 031e2ccb1fb6
line wrap: on
line diff
--- a/Syncopation models/LHL.py	Fri Mar 21 15:49:46 2014 +0000
+++ b/Syncopation models/LHL.py	Sun Oct 05 21:52:41 2014 +0100
@@ -1,218 +1,67 @@
 '''
 Author: Chunyang Song
 Institution: Centre for Digital Music, Queen Mary University of London
+'''
 
-** Longuet-Higgins and Lee's Model (LHL) **
+from basic_functions import concatenate, repeat, subdivide, ceiling
 
-Algorithm:
+terminal_nodes = []		# Global variable, storing all the terminal nodes from recursive tree structure in time order
 
-Only applicable to simple rhtyhms.
+# Each terminnal node contains two properties: its node type (note or rest) and its metrical weight.
+class Node:
+	def __init__(self,node_type,metrical_weight):
+		self.node_type = node_type
+		self.metrical_weight = metrical_weight
 
-Generate tree structure:
-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]; 
-2) If after subdivision, a node has at least one branch that is full rest or one note, then it cannot be subdivided;
-3) Weight of node, initialized as 0, decrease by 1 with increasing depth of the tree.
+# This function will recurse the tree for a binary sequence and return a sequence containing the terminal nodes in time order.
+def recursive_tree(seq, subdivision_seq, weight_seq, metrical_weight, level):
+	if seq == concatenate([1],repeat([0],len(seq)-1)):	# If matching to a Note type, add to terminal nodes
+		terminal_nodes.append(Node('N',metrical_weight))
 
-Assign each node in the tree three attributes - weight, pos, event;
-Search for Note-Rest pairs that Note's weight is no more than Rest's weight;
-Syncopation for such a pair is the difference in weights; 
-Total Syncopation value for non-syncopation rhythms is -1; for syncopated rhythms is the sum of syncopation of all notes.
+	elif seq == repeat([0],len(seq)):					# If matching to a Rest type, add to terminal nodes
+		terminal_nodes.append(Node('R',metrical_weight))
 
-In order to evaluate LHL models with the others, re-scale LHL's measures to non-negative.
-Apart from the immeasuable rhythms, add 1 to each syncopation value. 
+	else:													# Keep subdividing by the subdivisor of the next level
+		sub_seq = subdivide(seq, subdivision_seq[level+1])	
+		sub_weight_seq = concatenate([metrical_weight],repeat([weight_seq[level+1]],subdivision_seq[level+1]-1))
+		for a in range(len(sub_seq)):
+			recursive_tree(sub_seq[a], subdivision_seq, weight_seq, sub_weight_seq[a], level+1)
 
-'''
-from MeterStructure import MeterStructure
+# This function calculate syncoaption score for LHL model. 
+def get_syncopation(seq, subdivision_seq, weight_seq, prebar_seq, rhythm_category):
+	syncopation = None
+	if rhythm_category == 'poly':
+		print 'Error: LHL model cannot deal with polyrhythms.'
+	else:
+		# If there is rhythm in previous bar, process its tree structure
+		if prebar_seq != None:		
+			recursive_tree(ceiling(prebar_seq), subdivision_seq, weight_seq, weight_seq[0],0)
+			
+			# Only keep the last note-type node
+			while terminal_nodes[-1].node_type != 'N':
+				del terminal_nodes[-1]
+			del terminal_nodes[0:-1]
 
-class Node:
-	def __init__(self, pos, event):
-		self.pos = pos
-		self.event = event
+		# For the rhythm in the current bar, process its tree structure and store the terminal nodes 
+		recursive_tree(ceiling(seq), subdivision_seq, weight_seq, weight_seq[0],0)
+		
+		# for t in terminal_nodes:
+		# 	print '<', t.node_type, t.metrical_weight, '>'
 
-	def assignWeight(self, time_sig):
-		ms = MeterStructure(time_sig)
-		# Retrieve the LHL meter hierarchy in one bar;
-		#(e.g. weight = [0, -4, -3, -4, -2, -4, -3, -4, -1, -4, -3, -4, -2, -4, -3, -4] in 4/4 meter)
-		weights = ms.getLHLWeights(1)	
-		
-		# find the metrical weight of the note that locates at certain position(self.pos)
-		# index is the corresponding position of "self.pos" in the "weights" list
-		index = int(((abs(self.pos)%48)/48.0)*len(weights))		
-		self.weight = weights[index]
+		# Search for the NR pairs that contribute to syncopation, add the weight-difference to the NR_pair_syncopation list
+		NR_pair_syncopation = []
+		for i in range(len(terminal_nodes)-1,0,-1):
+			if terminal_nodes[i].node_type == 'R':
+				for j in range(i-1, -1, -1):
+					if (terminal_nodes[j].node_type == 'N') & (terminal_nodes[i].metrical_weight >= terminal_nodes[j].metrical_weight):
+						NR_pair_syncopation.append(terminal_nodes[i].metrical_weight - terminal_nodes[j].metrical_weight)
+						break
+		#print NR_pair_syncopation
 
-class Tree:
-	def __init__(self):
-		self.nodes = []
+		# If no syncopation, the value is -1; otherwise, sum all the local syncopation values stored in NR_pair_syncopation list	
+		if len(NR_pair_syncopation) != 0:
+			syncopation = sum(NR_pair_syncopation)	
+		elif len(terminal_nodes) != 0:
+			syncopation = -1
 
-	def add(self, Node):
-		self.nodes.append (Node)
-
-	def getWeight(self, time_sig):
-		for n in self.nodes:
-			n.assignWeight(time_sig)
-
-	def display(self):
-		print 'Pos, Event, Weight'
-		for n in self.nodes:
-			print '%02d    %s      %02d' % (n.pos, n.event, n.weight)
-
-
-def subdivide(sequence, segments_num):
-	subSeq = []
-	if len(sequence) % segments_num != 0:
-		print 'Error: rhythm segment cannot be equally subdivided.'
-	else:
-		n = len(sequence) / segments_num
-		start , end = 0, n
-		for i in range(segments_num):
-			subSeq.append(sequence[start : end])
-			start = end
-			end = end + n
-	
-	return subSeq
-
-
-def eventType (sequence):
-	if not(1 in sequence):	
-		event = 'R'  # Full rest
-	elif sequence[0] == 1 and not(1 in sequence[1:]): 
-		event = 'N'  # Only one on-beat note
-	else:
-		event = 'D'  # Divisable
-
-	return event
-
-def splitInto2 (sequence, tree, pos, isRecursive):
-	
-	subs = subdivide(sequence, 2)
-	
-	for s in subs:
-		if eventType(s) == 'R' or eventType(s) == 'N':
-			#print 'test', s, eventType(s), pos
-			tree.add( Node(pos, eventType(s)) )
-		else:
-			if isRecursive:
-				#print 'test', s, eventType(s), pos
-				splitInto2 (s, tree, pos, True)
-			else:
-				splitInto3 (s, tree, pos)
-
-		pos = pos + len(sequence) / 2
-	
-	return tree
-
-def splitInto3 (sequence, tree, pos):
-
-	subs = subdivide(sequence, 3)
-
-	for s in subs:
-		if eventType(s) == 'R' or eventType(s) == 'N':
-			#print 'test', s, eventType(s), pos
-			tree.add( Node(pos, eventType(s)) )
-	
-		else:
-			splitInto2 (s, tree, pos, True)			
-		
-		pos = pos + len(sequence) / 3
-
-	return tree
-
-
-def createTree(rhythm, time_sig, bar):
-	t = Tree()
-	# The root is the rhtyhm in a entire bar, has weight 0, at position 0
-	weight, pos, event = 0 , 0, eventType(rhythm)
-	root = Node (pos, event)
-	if '2/4' in time_sig:
-		t.add ( Node(-24, 'N') )  # Treat the last metronome beat in the previous bar as a sounded note
-	
-		if event == 'D':
-			t = splitInto2(rhythm, t, pos, True)
-		else:
-			t.add (root)
-
-	elif '4/4' in time_sig:
-		t.add ( Node(-12, 'N') )  # Treat the last metronome beat in the previous bar as a sounded note
-	
-		if event == 'D':
-			t = splitInto2(rhythm, t, pos, True)
-		else:
-			t.add (root)
-
-	elif '3/4' in time_sig:
-		t.add ( Node(-8, 'N') )  # Treat the last metronome beat in the previous bar as a sounded note
-
-		segments = subdivide (rhythm, bar)
-
-		for s in segments:
-			if eventType(s) == 'D':
-				t = splitInto3(s, t, pos)
-			pos = pos + len(rhythm)/bar
-
-
-	elif '6/8' in time_sig:
-		t.add ( Node(-8, 'N') )  # Treat the last metronome beat in the previous bar as a sounded note
-		
-		segments = subdivide (rhythm, bar)
-
-		for s in segments:
-			if eventType(s) == 'D': 
-				t = splitInto2(s, t, pos, False)
-			pos = pos + len(rhythm)/bar
-
-	else:
-		print 'This time signature is not defined. Choose between 4/4, 3/4 or 6/8'
-
-	return t
-
-
-def lhl(rhythm, time_sig, category, bar):
-	measures = []
-
-	if 'poly' in category:
-		return -1
-	else:
-		tree = createTree(rhythm, time_sig, bar)	
-		tree.getWeight(time_sig)
-		#tree.display()
-
-		# find NR-pair that N's weight <= R's weight, add difference in weight to measures[] 
-		N_weight = tree.nodes[0].weight
-		size = len(tree.nodes)
-
-		for i in range(1, size):
-		
-			if tree.nodes[i].event == 'N':
-				N_weight = tree.nodes[i].weight
-
-			if tree.nodes[i].event == 'R' and tree.nodes[i].weight >= N_weight:
-				measures.append(tree.nodes[i].weight - N_weight )
-
-		# Calculate syncopation
-		if len(measures) == 0: # syncopation of non-syncopated rhythm is -1
-			syncopation = -1
-		else:
-			syncopation = sum(measures)  # syncopation of syncopated rhythm is the sum of measures[]
-		
-		return syncopation + 1 # For evaluation purpose, scale up by 1 to make scores above 0 
-
-
-# Retrieve the stimuli
-#f = file('stimuli.txt')
-f = file('stimuli_34only.txt')
-
-#Calculate syncopation for each rhythm pattern
-while True:
-	line = f.readline().split(';')
-	if len(line) == 1:
-		 break
-	else:
-		sti_name = line[0]
-		rhythmString = line[1].split()
-		time_sig = line[2]
-		category = line[3]
-		bar = int(line[4])
-		
-		rhythm = map(int,rhythmString[0].split(','))
-
-		print sti_name, lhl(rhythm, time_sig, category, bar)
\ No newline at end of file
+	return syncopation