improvnet / utils /utils.py
vedmistry's picture
initial ImprovNet HARP deployment
5f6e625
Raw
History Blame Contribute Delete
19.2 kB
import math
import pickle
import random
import copy
import numpy as np
import miditoolkit
import pandas as pd
import yaml
import os
from tqdm import tqdm
from music21 import converter, instrument, stream, note
import subprocess
from concurrent.futures import ProcessPoolExecutor
# from ssmnet.core import SsmNetDeploy
# Define a function to flatten the tokenized sequence
def flatten(sequence, add_special_tokens=True):
flattened_sequence = []
note_info = []
for i in range(len(sequence)):
if add_special_tokens:
if sequence[i] == "<T>" or sequence[i] == "<D>":
flattened_sequence.append(sequence[i])
if sequence[i][0] == "piano":
note_info.append(sequence[i][1])
note_info.append(sequence[i][2])
elif sequence[i][0] == "onset":
note_info.append(sequence[i][1])
elif sequence[i][0] == "dur":
note_info.append(sequence[i][1])
flattened_sequence.append(note_info)
note_info = []
return flattened_sequence
def parse_generation(sequence, add_special_tokens=True):
flattened_sequence = []
note_info = {}
for i in range(len(sequence)):
if add_special_tokens:
if sequence[i] == "<T>" or sequence[i] == "<D>":
flattened_sequence.append(sequence[i])
if sequence[i][0] == "piano":
note_info['pitch'] = sequence[i][1]
note_info['velocity'] = sequence[i][2]
# Arrange the note info in the following order: [pitch, velocity, onset, duration]
if len(note_info) == 4:
note_info = [note_info['pitch'], note_info['velocity'], note_info['onset'], note_info['duration']]
flattened_sequence.append(note_info)
note_info = {}
elif sequence[i][0] == "onset":
note_info['onset'] = sequence[i][1]
elif sequence[i][0] == "dur":
note_info['duration'] = sequence[i][1]
sequence = copy.deepcopy(flattened_sequence)
unflattened_sequence = []
for i in range(len(sequence)):
if sequence[i] == "<T>" or sequence[i] == "<D>":
unflattened_sequence.append(sequence[i])
continue
elif type(sequence[i]) == tuple:
unflattened_sequence.append(sequence[i])
else:
note_info = ("piano", sequence[i][0], sequence[i][1])
unflattened_sequence.append(note_info)
note_info = ("onset", sequence[i][2])
unflattened_sequence.append(note_info)
note_info = ("dur", sequence[i][3])
unflattened_sequence.append(note_info)
note_info = []
return unflattened_sequence
# Reverse the flattened function
def unflatten(sequence, static_velocity=False):
unflattened_sequence = []
for i in range(len(sequence)):
if sequence[i] == "<T>" or sequence[i] == "<D>":
unflattened_sequence.append(sequence[i])
continue
elif type(sequence[i]) == tuple:
unflattened_sequence.append(sequence[i])
else:
note_info = ("onset", sequence[i][2])
unflattened_sequence.append(note_info)
note_info = ("dur", sequence[i][3])
unflattened_sequence.append(note_info)
if static_velocity:
note_info = ("piano", sequence[i][0], 90)
else:
note_info = ("piano", sequence[i][0], sequence[i][1])
unflattened_sequence.append(note_info)
note_info = []
return unflattened_sequence
def unflatten_for_aria(sequence):
unflattened_sequence = []
for i in range(len(sequence)):
if type(sequence[i]) == str:
unflattened_sequence.append(sequence[i])
continue
elif type(sequence[i]) == tuple:
unflattened_sequence.append(sequence[i])
else:
note_info = ("piano", sequence[i][0], sequence[i][1])
unflattened_sequence.append(note_info)
note_info = ("onset", sequence[i][2])
unflattened_sequence.append(note_info)
note_info = ("dur", sequence[i][3])
unflattened_sequence.append(note_info)
note_info = []
return unflattened_sequence
# Reverse the corrupted flattened function
def unflatten_corrupted(sequence, static_velocity=False):
unflattened_sequence = []
for i in range(len(sequence)):
if type(sequence[i]) == str:
unflattened_sequence.append(sequence[i])
continue
elif type(sequence[i]) == tuple:
unflattened_sequence.append(sequence[i])
else:
if type(sequence[i][2]) == int:
note_info = ("onset", sequence[i][2])
else:
note_info = 'O'
unflattened_sequence.append(note_info)
if type(sequence[i][3]) == int:
note_info = ("dur", sequence[i][3])
else:
note_info = 'D'
unflattened_sequence.append(note_info)
if type(sequence[i][0]) == int:
if static_velocity:
note_info = ("piano", sequence[i][0], 90)
else:
note_info = ("piano", sequence[i][0], sequence[i][1])
else:
note_info = 'PVM'
unflattened_sequence.append(note_info)
note_info = []
return unflattened_sequence
class Segment_Novelty:
def __init__(self, config_file, audio_file):
with open(config_file, "r", encoding="utf-8") as fid:
config_d = yaml.safe_load(fid)
self.ssmnet = SsmNetDeploy(config_d)
self.audio_file = audio_file
def m_get_features(self, audio_file):
return self.ssmnet.m_get_features(audio_file)
def m_get_ssm_novelty(self, feat_3m):
return self.ssmnet.m_get_ssm_novelty(feat_3m)
def m_get_boundaries(self, hat_novelty_np, time_sec_v):
return self.ssmnet.m_get_boundaries(hat_novelty_np, time_sec_v)
def m_plot(self, hat_ssm_np, hat_novelty_np, hat_boundary_frame_v, output_pdf_file):
return self.ssmnet.m_plot(hat_ssm_np, hat_novelty_np, hat_boundary_frame_v, output_pdf_file)
def m_export_csv(self, hat_boundary_sec_v, output_csv_file):
return self.ssmnet.m_export_csv(hat_boundary_sec_v, output_csv_file)
def max_items(self, values, indices, n):
sorted_items = np.argsort(values)[::-1]
top_items = sorted_items[:n]
return indices[top_items]
def find_novelty_timestamps(self, top_peak_indices, timestampsarray):
return timestampsarray[top_peak_indices]
def locate_peak_timestamps(self, indices, values, n_peaks):
top_novelty_indices = self.max_items(values, indices, n_peaks)
_, time_sec_v = self.m_get_features(self.audio_file)
timestamps = self.find_novelty_timestamps(top_novelty_indices, time_sec_v)
sorted_timestamps = np.sort(timestamps)
return sorted_timestamps
def get_peak_timestamps(self, audio_file, n_peaks):
feat_3m, time_sec_v = self.m_get_features(audio_file)
_, hat_novelty_np = self.m_get_ssm_novelty(feat_3m)
_, hat_boundary_frame_v = self.m_get_boundaries(hat_novelty_np, time_sec_v)
all_novelty_values = hat_novelty_np[hat_boundary_frame_v]
return self.locate_peak_timestamps(hat_boundary_frame_v, all_novelty_values, n_peaks)
# Skyline function for separating melody and harmony from the tokenized sequence
def skyline(sequence: list, diff_threshold=50, static_velocity=True, pitch_threshold=None):
if pitch_threshold is None:
pitch_threshold = 0
melody = []
harmony = []
pointer_pitch = sequence[0][0]
pointer_velocity = sequence[0][1]
pointer_onset = sequence[0][2]
pointer_duration = sequence[0][3]
i = 0
for i in range(1, len(sequence)):
if type(sequence[i]) != str:
current_pitch = sequence[i][0]
current_velocity = sequence[i][1]
current_onset = sequence[i][2]
current_duration = sequence[i][3]
if type(sequence[i-1]) == str and type(sequence[i-2]) == str:
diff_curr_prev_onset = 5000
elif type(sequence[i-1]) == str and type(sequence[i-2]) != str:
diff_curr_prev_onset = abs(current_onset - sequence[i-2][2])
else:
diff_curr_prev_onset = abs(current_onset - sequence[i-1][2])
# Check if the difference between the current onset and the previous onset is greater than the threshold and the pitch is greater than the threshold
if diff_curr_prev_onset > diff_threshold:
if pointer_pitch > pitch_threshold:
# Append the previous note
if static_velocity:
melody.append([pointer_pitch, 90, pointer_onset, pointer_duration])
else:
melody.append([pointer_pitch, pointer_velocity, pointer_onset, pointer_duration])
# Update the pointer
pointer_pitch = current_pitch
pointer_velocity = current_velocity
pointer_onset = current_onset
pointer_duration = current_duration
else:
if current_pitch > pointer_pitch:
# Append the previous note
harmony.append(("piano", pointer_pitch, pointer_velocity))
harmony.append(("onset", pointer_onset))
harmony.append(("dur", pointer_duration))
# Append <t> based on condition
if current_onset < pointer_onset:
harmony.append("<T>")
# Update the pointer
pointer_pitch = current_pitch
pointer_velocity = current_velocity
pointer_onset = current_onset
pointer_duration = current_duration
else:
# Append the previous note
harmony.append(("piano", current_pitch, current_velocity))
harmony.append(("onset", current_onset))
harmony.append(("dur", current_duration))
# Append <t> based on condition
if current_onset < pointer_onset:
harmony.append("<T>")
continue
# Append the last note
if i == len(sequence) - 1:
if diff_curr_prev_onset > diff_threshold:
if pointer_pitch > pitch_threshold:
if static_velocity:
melody.append([pointer_pitch, 90, pointer_onset, pointer_duration])
else:
melody.append([pointer_pitch, pointer_velocity, pointer_onset, pointer_duration])
else:
if current_pitch > pointer_pitch:
if current_pitch > pitch_threshold:
if static_velocity:
melody.append(["piano", current_pitch, 90, current_onset, current_duration])
else:
melody.append(["piano", current_pitch, current_velocity, current_onset, current_duration])
else:
harmony.append(("piano", current_pitch, current_velocity))
harmony.append(("onset", current_onset))
harmony.append(("dur", current_duration))
if sequence[i-1] == "<T>":
melody.append("<T>")
if sequence[i] == "<D>":
melody.append("<D>")
return melody, harmony
# Define a function to round a value to the nearest 05
def round_to_nearest_n(input_value, round_to=0.05):
rounded_value = round(round(input_value / round_to) * round_to, 2)
return rounded_value
def get_chord_info(chunk):
if len(chunk) < 2:
return 0, 0, pd.DataFrame()
df = pd.DataFrame(chunk, columns=["pitch", "velocity", "onset", "duration"])
df['previous_onset'] = df['onset'].shift(1).fillna(0).astype(int)
df['next_onset'] = df['onset'].shift(-1).fillna(0).astype(int)
df['same_onset_previous'] = np.where((abs(df['onset'] - df['previous_onset']) <= 30), 1, 0)
df['same_onset_next'] = np.where((abs(df['onset'] - df['next_onset']) <= 30), 1, 0)
df['same_onset'] = np.where((df['same_onset_previous'] == 0) & (df['same_onset_next'] == 1), 1, 0)
counter = 0
group = 0
new_column = []
for value in df['same_onset']:
if value == 1:
counter += 1
group = counter
new_column.append(group)
df['new_same_onset'] = np.where((df['same_onset_previous'] == 0) & (df['same_onset_next'] == 0), 0, new_column)
len_df = len(df)
df.fillna(0, inplace=True)
df_filtered = df.loc[df['new_same_onset']!=0]
if len(df_filtered) == 0:
return 0, 0, pd.DataFrame()
cfr = len(df_filtered) / len_df
cd = df_filtered['new_same_onset'].mean()
cd = 8 if cd > 8 else cd
cfr = round_to_nearest_n(cfr, round_to=0.05)
cd = round_to_nearest_n(cd, round_to=0.25)
return cfr, cd, df
def get_conditions(separated_list):
cfr_list = []
cd_list = []
for i in range(len(separated_list)):
cfr, cd, df = get_chord_info(separated_list[i])
cfr_list.append(("cfr", cfr))
cd_list.append(("cd", cd))
return cfr_list, cd_list
# Separate the list of lists based on the <T> token
def separate_list(sequence):
separated_list = []
sublist = []
for i in range(len(sequence)):
if sequence[i] == "<T>":
separated_list.append(sublist)
sublist = []
elif type(sequence[i]) == list:
sublist.append(sequence[i])
if sublist:
separated_list.append(sublist)
return separated_list
def interleave_conditions(flattened_sequence, cfr_list, cd_list):
conditioned_flattened_sequence = []
for n, i in enumerate(flattened_sequence):
if n == 0:
cfr_condition = cfr_list.pop(0)
cd_condition = cd_list.pop(0)
conditioned_flattened_sequence.append(cfr_condition)
conditioned_flattened_sequence.append(cd_condition)
conditioned_flattened_sequence.append(i)
elif i == "<T>":
conditioned_flattened_sequence.append(i)
if len(cfr_list) > 0:
cfr_condition = cfr_list.pop(0)
cd_condition = cd_list.pop(0)
conditioned_flattened_sequence.append(cfr_condition)
conditioned_flattened_sequence.append(cd_condition)
else:
conditioned_flattened_sequence.append(i)
if len(cfr_list) > 0:
conditioned_flattened_sequence.append(cfr_list.pop(0))
conditioned_flattened_sequence.append(cd_list.pop(0))
return conditioned_flattened_sequence
def save_wav(filepath, soundfont_path="soundfont.sf"):
# Extract the directory and the stem (filename without extension)
directory = os.path.dirname(filepath)
stem = os.path.splitext(os.path.basename(filepath))[0]
# Construct the full paths for MIDI and WAV files
midi_filepath = os.path.join(directory, f"{stem}.mid")
wav_filepath = os.path.join(directory, f"{stem}.wav")
# Run the fluidsynth command to convert MIDI to WAV
process = subprocess.Popen(
f"fluidsynth -r 48000 {soundfont_path} -g 1.0 --quiet --no-shell {midi_filepath} -T wav -F {wav_filepath} > /dev/null",
shell=True
)
# -o synth.cpu-cores=6
process.wait()
return wav_filepath
def convert_midi_to_wav(filepaths, soundfont_path="../artifacts/soundfont.sf", max_workers=32, verbose=True):
if verbose:
if max_workers == 1:
results = []
for filepath in tqdm(filepaths, desc="Converting MIDI to WAV"):
results.append(save_wav(filepath, soundfont_path))
else:
with ProcessPoolExecutor(max_workers=max_workers) as executor:
# Use tqdm to track progress
results = list(tqdm(executor.map(save_wav, filepaths, [soundfont_path]*len(filepaths)), total=len(filepaths), desc="Converting MIDI to WAV"))
else:
if max_workers == 1:
results = []
for filepath in filepaths:
results.append(save_wav(filepath, soundfont_path))
else:
with ProcessPoolExecutor(max_workers=max_workers) as executor:
results = list(executor.map(save_wav, filepaths, [soundfont_path]*len(filepaths)))
return results
def xml_to_monophonic_midi(musicxml_file, midi_output_file):
# Load the MusicXML file
score = converter.parse(musicxml_file)
# Assuming the monophonic melody is in the first part (usually in leadsheets)
# You may need to adjust if the monophonic part is in a different part
melody_part = score.parts[0]
# Filter out any chord symbols (only keeping monophonic notes)
melody_notes = stream.Stream()
# Iterate through elements and only add individual notes, ignoring chords and other elements
for elem in melody_part.flat.notesAndRests: # 'flat' allows for easier access to all notes/rests
if isinstance(elem, note.Note): # Add only individual notes, no chords
melody_notes.append(elem)
elif isinstance(elem, note.Rest): # If you want to keep rests in the melody
melody_notes.append(elem)
# Set the instrument to Piano
piano_instrument = instrument.Piano()
melody_notes.insert(0, piano_instrument)
# Save the melody as a MIDI file
melody_notes.write('midi', midi_output_file)
print(f"Monophonic melody saved as {midi_output_file}")
def xml_to_midi(musicxml_file, midi_output_file):
# Load the MusicXML file
score = converter.parse(musicxml_file)
# Print the part names (optional, for debugging)
print(f"Loaded parts: {[p.partName for p in score.parts]}")
# Create a new stream to hold all converted parts
piano_score = stream.Stream()
# Loop through each part in the score
for part in score.parts:
# Create a new stream for the piano part
piano_part = stream.Part()
# Set the instrument to piano (MIDI program number for acoustic piano is 0)
piano_instrument = instrument.Piano()
piano_part.insert(0, piano_instrument)
# Add all the notes and rests from the original part to the new piano part
for elem in part.flat.notesAndRests:
piano_part.append(elem)
# Append the piano part to the new score
piano_score.append(piano_part)
# Save the entire score as a MIDI file
piano_score.write('midi', midi_output_file)
print(f"All tracks saved as piano MIDI in {midi_output_file}")