|
|
import numpy as np
|
|
|
import scipy.interpolate
|
|
|
from anticipation.convert import midi_to_events
|
|
|
from anticipation.config import *
|
|
|
from anticipation.vocab import *
|
|
|
from itertools import combinations
|
|
|
|
|
|
def load_annotation_file(file_path):
|
|
|
annotations = []
|
|
|
|
|
|
with open(file_path, 'r') as f:
|
|
|
for line in f:
|
|
|
parts = line.strip().split('\t')
|
|
|
if len(parts) >= 3:
|
|
|
timestamp = float(parts[0])
|
|
|
annotation = parts[2]
|
|
|
annotations.append((timestamp, annotation))
|
|
|
|
|
|
return annotations
|
|
|
|
|
|
def compare_annotations(file1_path, file2_path, interpolate=True):
|
|
|
"""
|
|
|
Creates a mapping between downbeat and beat times in two annotation files.
|
|
|
Inputs are timestamps in the first file, outputs are timestamps in the second file
|
|
|
"""
|
|
|
|
|
|
annotations1 = load_annotation_file(file1_path)
|
|
|
annotations2 = load_annotation_file(file2_path)
|
|
|
|
|
|
min_length = min(len(annotations1), len(annotations2))
|
|
|
if len(annotations1) != len(annotations2):
|
|
|
shorter_file = file1_path if len(annotations1) == min_length else file2_path
|
|
|
print(f'Number of annotations in {file1_path} and {file2_path} do not match.')
|
|
|
print(f"Proceeding with the first {min_length} annotations from {shorter_file}.")
|
|
|
|
|
|
|
|
|
data = []
|
|
|
|
|
|
for i in range(min_length):
|
|
|
data.append((annotations1[i][0], annotations2[i][0]))
|
|
|
|
|
|
x,y = list(zip(*data))
|
|
|
|
|
|
if interpolate:
|
|
|
map = scipy.interpolate.interp1d(x, y)
|
|
|
|
|
|
return map
|
|
|
|
|
|
else:
|
|
|
return x,y
|
|
|
|
|
|
def power_set(lst, min_length=2, max_length=6):
|
|
|
result = []
|
|
|
|
|
|
for i in range(min_length, min(max_length + 1, len(lst) + 1)):
|
|
|
result.extend(combinations(lst, i))
|
|
|
return result
|
|
|
|
|
|
def align_tokens(file1, file2, file3, file4, skip_Nones=True):
|
|
|
|
|
|
perf = midi_to_events(file1, quantize=False)
|
|
|
score = midi_to_events(file2, quantize=False)
|
|
|
|
|
|
p_beats, s_beats = compare_annotations(file3,file4,interpolate=False)
|
|
|
s_beats = np.array(s_beats)
|
|
|
p_beats = np.array(p_beats)
|
|
|
map = compare_annotations(file3, file4)
|
|
|
|
|
|
|
|
|
p_tuples = [[perf[3*i]/TIME_RESOLUTION, perf[3*i+1] - DUR_OFFSET, perf[3*i+2] - NOTE_OFFSET] for i in range(int(len(perf)/3))]
|
|
|
s_tuples = [[score[3*i]/TIME_RESOLUTION, score[3*i+1] - DUR_OFFSET, score[3*i+2] - NOTE_OFFSET] for i in range(int(len(score)/3))]
|
|
|
p_times = [tup[0] for tup in p_tuples]
|
|
|
s_times = [tup[0] for tup in s_tuples]
|
|
|
|
|
|
tol = 1e-4
|
|
|
|
|
|
|
|
|
s_tuples_b = []
|
|
|
assigned = []
|
|
|
|
|
|
for tup in s_tuples:
|
|
|
mask = np.abs(tup[0] - s_beats) <= tol
|
|
|
if sum(mask):
|
|
|
beat = list(np.where(mask)[0])[0]
|
|
|
s_tuples_b.append((tup[0], tup[1], tup[2], beat))
|
|
|
assigned.append(beat)
|
|
|
else:
|
|
|
s_tuples_b.append(tup)
|
|
|
|
|
|
for i in range(len(s_beats)):
|
|
|
if i not in assigned:
|
|
|
print(f'could not find notes in score associated with beat {i}')
|
|
|
|
|
|
|
|
|
p_tuples_b = []
|
|
|
assigned = []
|
|
|
|
|
|
for tup in p_tuples:
|
|
|
mask = np.abs(tup[0] - p_beats) <= tol
|
|
|
if sum(mask):
|
|
|
beat = list(np.where(mask)[0])[0]
|
|
|
p_tuples_b.append((tup[0], tup[1], tup[2], beat))
|
|
|
assigned.append(beat)
|
|
|
else:
|
|
|
p_tuples_b.append(tup)
|
|
|
|
|
|
for j in [i for i in range(len(p_beats)) if i not in assigned]:
|
|
|
beat = p_beats[j]
|
|
|
|
|
|
candidates = [tup[0] for tup in p_tuples_b if len(tup)==3 and abs(tup[0]-beat)<=0.5]
|
|
|
|
|
|
success = False
|
|
|
for subset in power_set(candidates):
|
|
|
if np.abs(np.average(subset) - beat) <= tol:
|
|
|
for time in subset:
|
|
|
k = p_times.index(time)
|
|
|
p_tuples_b[k] = (p_tuples_b[k][0], p_tuples_b[k][1], p_tuples_b[k][2], j)
|
|
|
success = True
|
|
|
|
|
|
break
|
|
|
if not success:
|
|
|
print(f'could not find notes in perf associated with beat {j}')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
matched_tuples = []
|
|
|
|
|
|
s_tuples_b_copy = s_tuples_b.copy()
|
|
|
|
|
|
p_min = map.x.min()
|
|
|
p_max = map.x.max()
|
|
|
s_min = map.y.min()
|
|
|
s_max = map.y.max()
|
|
|
|
|
|
for i, p_tuple in enumerate(p_tuples_b):
|
|
|
for j, s_tuple in enumerate(s_tuples_b_copy):
|
|
|
|
|
|
p_time, p_note = p_tuple[0], p_tuple[2]
|
|
|
s_time, s_note = s_tuple[0], s_tuple[2]
|
|
|
|
|
|
k = s_tuples_b.index(s_tuple)
|
|
|
|
|
|
if len(p_tuple) == 4 and len(s_tuple) == 4 and p_tuple[2:] == s_tuple[2:]:
|
|
|
matched_tuples.append([p_tuple,i,s_tuple,k])
|
|
|
s_tuples_b_copy.remove(s_tuple)
|
|
|
elif len(p_tuple) == 3 and len(s_tuple) == 3 and p_time < p_min and s_time < s_min and p_note == s_note:
|
|
|
matched_tuples.append([p_tuple,i,s_tuple,k])
|
|
|
s_tuples_b_copy.remove(s_tuple)
|
|
|
elif len(p_tuple) == 3 and len(s_tuple) == 3 and p_time > p_max and s_time > s_max and p_note == s_note:
|
|
|
matched_tuples.append([p_tuple,i,s_tuple,k])
|
|
|
s_tuples_b_copy.remove(s_tuple)
|
|
|
elif len(p_tuple) == 3 and len(s_tuple) == 3 and p_min <= p_time <= p_max and s_min <= s_time <= s_max and \
|
|
|
np.abs(map(p_time) - s_time) < .1 and p_note == s_note:
|
|
|
matched_tuples.append([p_tuple,i,s_tuple,k])
|
|
|
s_tuples_b_copy.remove(s_tuple)
|
|
|
if p_tuple not in [l[0] for l in matched_tuples] and not skip_Nones:
|
|
|
matched_tuples.append([p_tuple,i,[None,None,None],None])
|
|
|
|
|
|
|
|
|
for i, l in enumerate(matched_tuples):
|
|
|
|
|
|
l[0] = [round(l[0][0]*TIME_RESOLUTION), l[0][1]+DUR_OFFSET, l[0][2]+NOTE_OFFSET]
|
|
|
l[0] = [CONTROL_OFFSET + t for t in l[0]]
|
|
|
|
|
|
if l[2][0] != None:
|
|
|
l[2] = [round(l[2][0]*TIME_RESOLUTION), l[2][1]+DUR_OFFSET, l[2][2]+NOTE_OFFSET]
|
|
|
matched_tuples[i] = l
|
|
|
|
|
|
return matched_tuples
|
|
|
|
|
|
def align_tokens2(file1, file2, file3, file4, skip_Nones=True, thres=0.1):
|
|
|
|
|
|
perf = midi_to_events(file1, quantize=False)
|
|
|
score = midi_to_events(file2, quantize=False)
|
|
|
|
|
|
p_beats, s_beats = compare_annotations(file3,file4,interpolate=False)
|
|
|
s_beats = np.array(s_beats)
|
|
|
p_beats = np.array(p_beats)
|
|
|
map = compare_annotations(file3, file4)
|
|
|
|
|
|
|
|
|
p_tuples = [[perf[3*i]/TIME_RESOLUTION, perf[3*i+1] - DUR_OFFSET, perf[3*i+2] - NOTE_OFFSET] for i in range(int(len(perf)/3))]
|
|
|
s_tuples = [[score[3*i]/TIME_RESOLUTION, score[3*i+1] - DUR_OFFSET, score[3*i+2] - NOTE_OFFSET] for i in range(int(len(score)/3))]
|
|
|
|
|
|
matched_tuples = []
|
|
|
|
|
|
s_tuples_copy = s_tuples.copy()
|
|
|
|
|
|
p_min = map.x.min()
|
|
|
p_max = map.x.max()
|
|
|
|
|
|
for i, p_tuple in enumerate(p_tuples):
|
|
|
best_dist = np.inf
|
|
|
best_match = [None, None, None]
|
|
|
best_index = None
|
|
|
|
|
|
p_time, p_note = p_tuple[0], p_tuple[2]
|
|
|
|
|
|
if p_min <= p_time <= p_max:
|
|
|
|
|
|
for j, s_tuple in enumerate(s_tuples_copy):
|
|
|
|
|
|
s_time, s_note = s_tuple[0], s_tuple[2]
|
|
|
|
|
|
k = s_tuples.index(s_tuple)
|
|
|
|
|
|
dist = np.abs(map(p_time) - s_time)
|
|
|
|
|
|
if p_note != s_note:
|
|
|
continue
|
|
|
|
|
|
if dist <= thres and dist <= best_dist:
|
|
|
best_dist = dist
|
|
|
best_match = s_tuple
|
|
|
best_index = k
|
|
|
|
|
|
if best_index is not None:
|
|
|
matched_tuples.append([p_tuple,i,best_match,best_index])
|
|
|
s_tuples_copy.remove(best_match)
|
|
|
elif not skip_Nones:
|
|
|
matched_tuples.append([p_tuple,i,best_match,best_index])
|
|
|
|
|
|
|
|
|
|
|
|
for i, l in enumerate(matched_tuples):
|
|
|
|
|
|
l[0] = [round(l[0][0]*TIME_RESOLUTION), l[0][1]+DUR_OFFSET, l[0][2]+NOTE_OFFSET]
|
|
|
l[0] = [CONTROL_OFFSET + t for t in l[0]]
|
|
|
|
|
|
if l[2][0] != None:
|
|
|
l[2] = [round(l[2][0]*TIME_RESOLUTION), l[2][1]+DUR_OFFSET, l[2][2]+NOTE_OFFSET]
|
|
|
matched_tuples[i] = l
|
|
|
|
|
|
return matched_tuples |