TastyPiano / src /music /pipeline /processed2handcodedrep.py
Cédric Colas
initial commit
e775f6d
raw
history blame
17.8 kB
import numpy as np
from music21 import *
from music21.features import native, jSymbolic, DataSet
import pretty_midi as pm
from src.music.utils import get_out_path
from src.music.utilities.handcoded_rep_utilities.tht import tactus_hypothesis_tracker, tracker_analysis
from src.music.utilities.handcoded_rep_utilities.loudness import get_loudness, compute_total_loudness, amplitude2db, velocity2amplitude, get_db_of_equivalent_loudness_at_440hz, pitch2freq
import json
import os
environment.set('musicxmlPath', '/home/cedric/Desktop/test/')
midi_path = "/home/cedric/Documents/pianocktail/data/music/processed/doug_mckenzie_processed/allthethings_reharmonized_processed.mid"
FEATURES_DICT_SCORE = dict(
# strongest pulse: measures how fast the melody is
# stronger_pulse=jSymbolic.StrongestRhythmicPulseFeature,
# weights of the two strongest pulse, measures rhythmic consistency: https://web.mit.edu/music21/doc/moduleReference/moduleFeaturesJSymbolic.html#combinedstrengthoftwostrongestrhythmicpulsesfeature
pulse_strength_two=jSymbolic.CombinedStrengthOfTwoStrongestRhythmicPulsesFeature,
# weights of the strongest pulse, measures rhythmic consistency: https://web.mit.edu/music21/doc/moduleReference/moduleFeaturesJSymbolic.html#combinedstrengthoftwostrongestrhythmicpulsesfeature
pulse_strength = jSymbolic.StrengthOfStrongestRhythmicPulseFeature,
# variability of attacks: https://web.mit.edu/music21/doc/moduleReference/moduleFeaturesJSymbolic.html#variabilityoftimebetweenattacksfeature
)
FEATURES_DICT = dict(
# bass register importance: https://web.mit.edu/music21/doc/moduleReference/moduleFeaturesJSymbolic.html#importanceofbassregisterfeature
# bass_register=jSymbolic.ImportanceOfBassRegisterFeature,
# high register importance: https://web.mit.edu/music21/doc/moduleReference/moduleFeaturesJSymbolic.html#importanceofbassregisterfeature
# high_register=jSymbolic.ImportanceOfHighRegisterFeature,
# medium register importance: https://web.mit.edu/music21/doc/moduleReference/moduleFeaturesJSymbolic.html#importanceofbassregisterfeature
# medium_register=jSymbolic.ImportanceOfMiddleRegisterFeature,
# number of common pitches (at least 9% of all): https://web.mit.edu/music21/doc/moduleReference/moduleFeaturesJSymbolic.html#numberofcommonmelodicintervalsfeature
# common_pitches=jSymbolic.NumberOfCommonPitchesFeature,
# pitch class variety (used at least once): https://web.mit.edu/music21/doc/moduleReference/moduleFeaturesJSymbolic.html#pitchvarietyfeature
# pitch_variety=jSymbolic.PitchVarietyFeature,
# attack_variability = jSymbolic.VariabilityOfTimeBetweenAttacksFeature,
# staccato fraction: https://web.mit.edu/music21/doc/moduleReference/moduleFeaturesJSymbolic.html#staccatoincidencefeature
# staccato_score = jSymbolic.StaccatoIncidenceFeature,
# mode analysis: https://web.mit.edu/music21/doc/moduleReference/moduleFeaturesNative.html
av_melodic_interval = jSymbolic.AverageMelodicIntervalFeature,
# chromatic motion: https://web.mit.edu/music21/doc/moduleReference/moduleFeaturesJSymbolic.html#chromaticmotionfeature
chromatic_motion = jSymbolic.ChromaticMotionFeature,
# direction of motion (fraction of rising intervals: https://web.mit.edu/music21/doc/moduleReference/moduleFeaturesJSymbolic.html#directionofmotionfeature
motion_direction = jSymbolic.DirectionOfMotionFeature,
# duration of melodic arcs: https://web.mit.edu/music21/doc/moduleReference/moduleFeaturesJSymbolic.html#durationofmelodicarcsfeature
melodic_arcs_duration = jSymbolic.DurationOfMelodicArcsFeature,
# melodic arcs size: https://web.mit.edu/music21/doc/moduleReference/moduleFeaturesJSymbolic.html#sizeofmelodicarcsfeature
melodic_arcs_size = jSymbolic.SizeOfMelodicArcsFeature,
# number of common melodic interval (at least 9% of all): https://web.mit.edu/music21/doc/moduleReference/moduleFeaturesJSymbolic.html#numberofcommonmelodicintervalsfeature
# common_melodic_intervals = jSymbolic.NumberOfCommonMelodicIntervalsFeature,
# https://web.mit.edu/music21/doc/moduleReference/moduleFeaturesJSymbolic.html#amountofarpeggiationfeature
# arpeggiato=jSymbolic.AmountOfArpeggiationFeature,
)
def compute_beat_info(onsets):
onsets_in_ms = np.array(onsets) * 1000
tht = tactus_hypothesis_tracker.default_tht()
trackers = tht(onsets_in_ms)
top_hts = tracker_analysis.top_hypothesis(trackers, len(onsets_in_ms))
beats = tracker_analysis.produce_beats_information(onsets_in_ms, top_hts, adapt_period=250 is not None,
adapt_phase=tht.eval_f, max_delta_bpm=250, avoid_quickturns=None)
tempo = 1 / (np.mean(np.diff(beats)) / 1000) * 60 # in bpm
conf_values = tracker_analysis.tht_tracking_confs(trackers, len(onsets_in_ms))
pulse_clarity = np.mean(np.array(conf_values), axis=0)[1]
return tempo, pulse_clarity
def dissonance_score(A):
"""
Given a piano-roll indicator matrix representation of a musical work (128 pitches x beats),
return the dissonance as a function of beats.
Input:
A - 128 x beats indicator matrix of MIDI pitch number
"""
freq_rats = np.arange(1, 7) # Harmonic series ratios
amps = np.exp(-.5 * freq_rats) # Partial amplitudes
F0 = 8.1757989156 # base frequency for MIDI (note 0)
diss = [] # List for dissonance values
thresh = 1e-3
for beat in A.T:
idx = np.where(beat>thresh)[0]
if len(idx):
freqs, mags = [], [] # lists for frequencies, mags
for i in idx:
freqs.extend(F0*2**(i/12.0)*freq_rats)
mags.extend(amps)
freqs = np.array(freqs)
mags = np.array(mags)
sortIdx = freqs.argsort()
d = compute_dissonance(freqs[sortIdx],mags[sortIdx])
diss.extend([d])
else:
diss.extend([-1]) # Null value
diss = np.array(diss)
return diss[np.where(diss != -1)]
def compute_dissonance(freqs, amps):
"""
From https://notebook.community/soundspotter/consonance/week1_consonance
Compute dissonance between partials with center frequencies in freqs, uses a model of critical bandwidth.
and amplitudes in amps. Based on Sethares "Tuning, Timbre, Spectrum, Scale" (1998) after Plomp and Levelt (1965)
inputs:
freqs - list of partial frequencies
amps - list of corresponding amplitudes [default, uniformly 1]
"""
b1, b2, s1, s2, c1, c2, Dstar = (-3.51, -5.75, 0.0207, 19.96, 5, -5, 0.24)
f = np.array(freqs)
a = np.array(amps)
idx = np.argsort(f)
f = f[idx]
a = a[idx]
N = f.size
D = 0
for i in range(1, N):
Fmin = f[ 0 : N - i ]
S = Dstar / ( s1 * Fmin + s2)
Fdif = f[ i : N ] - f[ 0 : N - i ]
am = a[ i : N ] * a[ 0 : N - i ]
Dnew = am * (c1 * np.exp (b1 * S * Fdif) + c2 * np.exp(b2 * S * Fdif))
D += Dnew.sum()
return D
def store_new_midi(notes, out_path):
midi = pm.PrettyMIDI()
midi.instruments.append(pm.Instrument(program=0, is_drum=False))
midi.instruments[0].notes = notes
midi.write(out_path)
return midi
def processed2handcodedrep(midi_path, handcoded_rep_path=None, crop=30, verbose=False, save=True, return_rep=False, level=0):
try:
if not handcoded_rep_path:
handcoded_rep_path, _, _ = get_out_path(in_path=midi_path, in_word='processed', out_word='handcoded_reps', out_extension='.mid')
features = dict()
if verbose: print(' ' * level + 'Computing handcoded representations')
if os.path.exists(handcoded_rep_path):
with open(handcoded_rep_path.replace('.mid', '.json'), 'r') as f:
features = json.load(f)
rep = np.array([features[k] for k in sorted(features.keys())])
if rep.size == 49:
os.remove(handcoded_rep_path)
else:
if verbose: print(' ' * (level + 2) + 'Already computed.')
if return_rep:
return handcoded_rep_path, np.array([features[k] for k in sorted(features.keys())]), ''
else:
return handcoded_rep_path, ''
midi = pm.PrettyMIDI(midi_path) # load midi with pretty midi
notes = midi.instruments[0].notes # get notes
notes.sort(key=lambda x: (x.start, x.pitch)) # sort notes per start and pitch
onsets, offsets, pitches, durations, velocities = [], [], [], [], []
n_notes_cropped = len(notes)
for i_n, n in enumerate(notes):
onsets.append(n.start)
offsets.append(n.end)
durations.append(n.end-n.start)
pitches.append(n.pitch)
velocities.append(n.velocity)
if crop is not None: # find how many notes to keep
if n.start > crop and n_notes_cropped == len(notes):
n_notes_cropped = i_n
break
notes = notes[:n_notes_cropped]
midi = store_new_midi(notes, handcoded_rep_path)
# pianoroll = midi.get_piano_roll() # extract piano roll representation
# compute loudness
amplitudes = velocity2amplitude(np.array(velocities))
power_dbs = amplitude2db(amplitudes)
frequencies = pitch2freq(np.array(pitches))
loudness_values = get_loudness(power_dbs, frequencies)
# compute average perceived loudness
# for each power, compute loudness, then compute power such that the loudness at 440 Hz would be equivalent.
# equivalent_powers_dbs = get_db_of_equivalent_loudness_at_440hz(frequencies, power_dbs)
# then get the corresponding amplitudes
# equivalent_amplitudes = 10 ** (equivalent_powers_dbs / 20)
# not use a amplitude model across the sample to compute the instantaneous amplitude, turn it back to dbs, then to perceived loudness with unique freq 440 Hz
# av_total_loudness, std_total_loudness = compute_total_loudness(equivalent_amplitudes, onsets, offsets)
end_time = np.max(offsets)
start_time = notes[0].start
score = converter.parse(handcoded_rep_path)
score.chordify()
notes_without_chords = stream.Stream(score.flatten().getElementsByClass('Note'))
velocities_wo_chords, pitches_wo_chords, amplitudes_wo_chords, dbs_wo_chords = [], [], [], []
frequencies_wo_chords, loudness_values_wo_chords, onsets_wo_chords, offsets_wo_chords, durations_wo_chords = [], [], [], [], []
for i_n in range(len(notes_without_chords)):
n = notes_without_chords[i_n]
velocities_wo_chords.append(n.volume.velocity)
pitches_wo_chords.append(n.pitch.midi)
onsets_wo_chords.append(n.offset)
offsets_wo_chords.append(onsets_wo_chords[-1] + n.seconds)
durations_wo_chords.append(n.seconds)
amplitudes_wo_chords = velocity2amplitude(np.array(velocities_wo_chords))
power_dbs_wo_chords = amplitude2db(amplitudes_wo_chords)
frequencies_wo_chords = pitch2freq(np.array(pitches_wo_chords))
loudness_values_wo_chords = get_loudness(power_dbs_wo_chords, frequencies_wo_chords)
# compute average perceived loudness
# for each power, compute loudness, then compute power such that the loudness at 440 Hz would be equivalent.
# equivalent_powers_dbs_wo_chords = get_db_of_equivalent_loudness_at_440hz(frequencies_wo_chords, power_dbs_wo_chords)
# then get the corresponding amplitudes
# equivalent_amplitudes_wo_chords = 10 ** (equivalent_powers_dbs_wo_chords / 20)
# not use a amplitude model across the sample to compute the instantaneous amplitude, turn it back to dbs, then to perceived loudness with unique freq 440 Hz
# av_total_loudness_wo_chords, std_total_loudness_wo_chords = compute_total_loudness(equivalent_amplitudes_wo_chords, onsets_wo_chords, offsets_wo_chords)
ds = DataSet(classLabel='test')
f = list(FEATURES_DICT.values())
ds.addFeatureExtractors(f)
ds.addData(notes_without_chords)
ds.process()
for k, f in zip(FEATURES_DICT.keys(), ds.getFeaturesAsList()[0][1:-1]):
features[k] = f
ds = DataSet(classLabel='test')
f = list(FEATURES_DICT_SCORE.values())
ds.addFeatureExtractors(f)
ds.addData(score)
ds.process()
for k, f in zip(FEATURES_DICT_SCORE.keys(), ds.getFeaturesAsList()[0][1:-1]):
features[k] = f
# # # # #
# Register features
# # # # #
# features['av_pitch'] = np.mean(pitches)
# features['std_pitch'] = np.std(pitches)
# features['range_pitch'] = np.max(pitches) - np.min(pitches) # aka ambitus
# # # # #
# Rhythmic features
# # # # #
# tempo, pulse_clarity = compute_beat_info(onsets[:n_notes_cropped])
# features['pulse_clarity'] = pulse_clarity
# features['tempo'] = tempo
features['tempo_pm'] = midi.estimate_tempo()
# # # # #
# Temporal features
# # # # #
features['av_duration'] = np.mean(durations)
# features['std_duration'] = np.std(durations)
features['note_density'] = len(notes) / (end_time - start_time)
# intervals_wo_chords = np.diff(onsets_wo_chords)
# articulations = [max((i-d)/i, 0) for d, i in zip(durations_wo_chords, intervals_wo_chords) if i != 0]
# features['articulation'] = np.mean(articulations)
# features['av_duration_wo_chords'] = np.mean(durations_wo_chords)
# features['std_duration_wo_chords'] = np.std(durations_wo_chords)
# # # # #
# Dynamics features
# # # # #
features['av_velocity'] = np.mean(velocities)
features['std_velocity'] = np.std(velocities)
features['av_loudness'] = np.mean(loudness_values)
# features['std_loudness'] = np.std(loudness_values)
features['range_loudness'] = np.max(loudness_values) - np.min(loudness_values)
# features['av_integrated_loudness'] = av_total_loudness
# features['std_integrated_loudness'] = std_total_loudness
# features['av_velocity_wo_chords'] = np.mean(velocities_wo_chords)
# features['std_velocity_wo_chords'] = np.std(velocities_wo_chords)
# features['av_loudness_wo_chords'] = np.mean(loudness_values_wo_chords)
# features['std_loudness_wo_chords'] = np.std(loudness_values_wo_chords)
features['range_loudness_wo_chords'] = np.max(loudness_values_wo_chords) - np.min(loudness_values_wo_chords)
# features['av_integrated_loudness'] = av_total_loudness_wo_chords
# features['std_integrated_loudness'] = std_total_loudness_wo_chords
# indices_with_intervals = np.where(intervals_wo_chords > 0.01)
# features['av_loudness_change'] = np.mean(np.abs(np.diff(np.array(loudness_values_wo_chords)[indices_with_intervals]))) # accentuation
# features['av_velocity_change'] = np.mean(np.abs(np.diff(np.array(velocities_wo_chords)[indices_with_intervals]))) # accentuation
# # # # #
# Harmony features
# # # # #
# get major_minor score: https://web.mit.edu/music21/doc/moduleReference/moduleAnalysisDiscrete.html
music_analysis = score.analyze('key')
major_score = None
minor_score = None
for a in [music_analysis] + music_analysis.alternateInterpretations:
if 'major' in a.__str__() and a.correlationCoefficient > 0:
major_score = a.correlationCoefficient
elif 'minor' in a.__str__() and a.correlationCoefficient > 0:
minor_score = a.correlationCoefficient
if major_score is not None and minor_score is not None:
break
features['major_minor'] = major_score / (major_score + minor_score)
features['tonal_certainty'] = music_analysis.tonalCertainty()
# features['av_sensory_dissonance'] = np.mean(dissonance_score(pianoroll))
#TODO only works for chords, do something with melodic intervals: like proportion that is not third, fifth or sevenths?
# # # # #
# Interval features
# # # # #
#https://web.mit.edu/music21/doc/moduleReference/moduleAnalysisPatel.html
# features['melodic_interval_variability'] = analysis.patel.melodicIntervalVariability(notes_without_chords)
# # # # #
# Suprize features
# # # # #
# https://web.mit.edu/music21/doc/moduleReference/moduleAnalysisMetrical.html
# analysis.metrical.thomassenMelodicAccent(notes_without_chords)
# melodic_accents = [n.melodicAccent for n in notes_without_chords]
# features['melodic_accent'] = np.mean(melodic_accents)
if save:
for k, v in features.items():
features[k] = float(features[k])
with open(handcoded_rep_path.replace('.mid', '.json'), 'w') as f:
json.dump(features, f)
else:
print(features)
if os.path.exists(handcoded_rep_path):
os.remove(handcoded_rep_path)
if verbose: print(' ' * (level + 2) + 'Success.')
if return_rep:
return handcoded_rep_path, np.array([features[k] for k in sorted(features.keys())]), ''
else:
return handcoded_rep_path, ''
except:
if verbose: print(' ' * (level + 2) + 'Failed.')
if return_rep:
return None, None, 'error'
else:
return None, 'error'
if __name__ == '__main__':
processed2handcodedrep(midi_path, '/home/cedric/Desktop/test.mid', save=False)