view Syncopation models/LHL.py @ 0:76ce27beba95

Have uploaded the syncopation dataset and audio wavefies.
author Chunyang Song <csong@eecs.qmul.ac.uk>
date Fri, 21 Mar 2014 15:49:46 +0000
parents
children b2da092dc2e0
line wrap: on
line source
'''
Author: Chunyang Song
Institution: Centre for Digital Music, Queen Mary University of London

** Longuet-Higgins and Lee's Model (LHL) **

Algorithm:

Only applicable to simple rhtyhms.

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.

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.

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. 

'''
from MeterStructure import MeterStructure

class Node:
	def __init__(self, pos, event):
		self.pos = pos
		self.event = event

	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]

class Tree:
	def __init__(self):
		self.nodes = []

	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)