def test_key_name_to_key_number(): # First, test that number->name->number works for key_number in range(24): assert pretty_midi.key_name_to_key_number( pretty_midi.key_number_to_key_name(key_number)) == key_number # Explicitly test all valid input key_pc = {'c': 0, 'd': 2, 'e': 4, 'f': 5, 'g': 7, 'a': 9, 'b': 11} for key in key_pc: for flatsharp, shift in zip(['', '#', 'b'], [0, 1, -1]): key_number = (key_pc[key] + shift) % 12 for space in [' ', '']: for mode in ['M', 'Maj', 'Major', 'maj', 'major']: assert pretty_midi.key_name_to_key_number( key + flatsharp + space + mode) == key_number # Also ensure uppercase key name plus no mode string assert pretty_midi.key_name_to_key_number( key.upper() + flatsharp + space) == key_number for mode in ['m', 'Min', 'Minor', 'min', 'minor']: assert pretty_midi.key_name_to_key_number( key + flatsharp + space + mode) == key_number + 12 assert pretty_midi.key_name_to_key_number( key + flatsharp + space) == key_number + 12 # Test some invalid inputs for invalid_key in ['C# m', 'C# ma', 'ba', 'bm m', 'f## Major', 'O']: with pytest.raises(ValueError): pretty_midi.key_name_to_key_number(invalid_key)
def test_key_name_to_key_number(): # First, test that number->name->number works for key_number in range(24): assert pretty_midi.key_name_to_key_number( pretty_midi.key_number_to_key_name(key_number)) == key_number # Explicitly test all valid input key_pc = {'c': 0, 'd': 2, 'e': 4, 'f': 5, 'g': 7, 'a': 9, 'b': 11} for key in key_pc: for flatsharp, shift in zip(['', '#', 'b'], [0, 1, -1]): key_number = (key_pc[key] + shift) % 12 for space in [' ', '']: for mode in ['M', 'Maj', 'Major', 'maj', 'major']: assert pretty_midi.key_name_to_key_number( key + flatsharp + space + mode) == key_number # Also ensure uppercase key name plus no mode string assert pretty_midi.key_name_to_key_number(key.upper() + flatsharp + space) == key_number for mode in ['m', 'Min', 'Minor', 'min', 'minor']: assert pretty_midi.key_name_to_key_number( key + flatsharp + space + mode) == key_number + 12 assert pretty_midi.key_name_to_key_number( key + flatsharp + space) == key_number + 12 # Test some invalid inputs for invalid_key in ['C# m', 'C# ma', 'ba', 'bm m', 'f## Major', 'O']: with pytest.raises(ValueError): pretty_midi.key_name_to_key_number(invalid_key)
def estimate_key_signature(pm, by_name=True, fs=100, vel_thresh=0): if len(pm.key_signature_changes) > 1: raise ValueError( "estimate_key function is for single key signature midi data.") # 相対chromaを計算 rel_chroma = relative_chroma(pm, fs=fs, vel_thresh=vel_thresh) if rel_chroma is None: return None, np.array([0] * 12) # 最もスケールに合致したシフト数を計算 max_score, max_shift = 0.0, 0 major_scale = [1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1] for s in range(12): shift_scale = np.roll(major_scale, s).astype(bool) score = sum(rel_chroma[shift_scale]) if score > max_score: max_score, max_shift = score, s # 1度が多ければMajor,6度が多ければminorとする if rel_chroma[max_shift] < rel_chroma[max_shift - 3]: key_number = (max_shift + -3) % 12 + 12 else: key_number = max_shift return pretty_midi.key_number_to_key_name( key_number) if by_name else key_number, rel_chroma
def get_key(midi_file): ''' Load in key labels from a MIDI file ''' # Load in MIDI object and grab key change events try: pm = pretty_midi.PrettyMIDI(midi_file) key_changes = pm.key_signature_changes # Convert each key change's number to a string (like 'C Major') # Also convert it to lowercase, for mir_eval's sake key_changes = [ pretty_midi.key_number_to_key_name(k.key_number).lower() for k in key_changes ] except: Warning('error with prettyMIDI') return None if key_changes: key = key_changes[0] tonic, mode = key.split(' ') key = tonic.capitalize() + ' ' + mode if key == 'C major': return None else: return key else: return None
def create_chords(chord_list): nums = [] all_keys = np.zeros((12, len(chord_list))) for i, chord in enumerate(chord_list): chord_root = chord.split()[0][0] nums.append(chord_root) num = pretty_midi.key_name_to_key_number(chord_root) all_keys[0, i] = num for i in range(1, 12): all_keys[i] = (all_keys[i-1] + 1) % 12 all_transposes = [] for i in range(all_keys.shape[0]): progression = [] for j in range(all_keys.shape[1]): root = pretty_midi.key_number_to_key_name(int(all_keys[i, j])) if root[1] == 'b': take = root[:2] else: take = root[0] chord_type = chord_list[j][1:] progression.append(f'{take}{chord_type}') all_transposes.append(progression) return all_transposes
def extract_ground_truth(diagnostics_group): """ Extract ground-truth information from one or more MIDI files about a single MIDI file based on the results in one or more diagnostics files and return a JAMS object with all of the annotations compiled. Parameters ---------- - diagnostics_group : list of dict List of dicts of diagnostics, each about a successful alignment of a different MIDI file to a single audio file. """ # Construct the JAMS object jam = jams.JAMS() # Load in the first diagnostics (doesn't matter which as they all # should correspond the same audio file) diagnostics = diagnostics_group[0] # Load in the audio file to get its duration for the JAMS file audio, fs = librosa.load(diagnostics['audio_filename'], feature_extraction.AUDIO_FS) jam.file_metadata.duration = librosa.get_duration(y=audio, sr=fs) # Also store metadata about the audio file, retrieved from the MSD jam.file_metadata.identifiers = {'track_id': diagnostics['audio_id']} jam.file_metadata.artist = MSD_LIST[diagnostics['audio_id']]['artist'] jam.file_metadata.title = MSD_LIST[diagnostics['audio_id']]['title'] # Iterate over the diagnostics files supplied for diagnostics in diagnostics_group: # Create annotation metadata object, shared across annotations commit = subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip() commit_url = "http://github.com/craffel/midi-dataset/tree/" + commit annotator = { 'midi_md5': diagnostics['midi_md5'], 'commit_url': commit_url, 'confidence': diagnostics['score'] } annotation_metadata = jams.AnnotationMetadata( curator=jams.Curator('Colin Raffel', '*****@*****.**'), version='0.0.1b', corpus='Million Song Dataset MIDI Matches', annotator=annotator, annotation_tools=( 'MIDI files were matched and aligned to audio files using the ' 'code at http://github.com/craffel/midi-dataset. Information ' 'was extracted from MIDI files using pretty_midi ' 'https://github.com/craffel/pretty-midi.'), annotation_rules=( 'Beat locations and key change times were linearly ' 'interpolated according to an audio-to-MIDI alignment.'), validation=( 'Only MIDI files with alignment confidence scores >= .5 were ' 'considered "correct". The confidence score can be used as a ' 'rough guide to the potential correctness of the annotation.'), data_source='Inferred from a MIDI file.') # Load the extracted features midi_features = deepdish.io.load(diagnostics['midi_features_filename']) audio_features = deepdish.io.load( diagnostics['audio_features_filename']) # Load in the original MIDI file midi_object = pretty_midi.PrettyMIDI(diagnostics['midi_filename']) # Compute the times of the frames (will be used for interpolation) midi_frame_times = feature_extraction.frame_times( midi_features['gram'])[diagnostics['aligned_midi_indices']] audio_frame_times = feature_extraction.frame_times( audio_features['gram'])[diagnostics['aligned_audio_indices']] # Get the interpolated beat locations and add them to the JAM adjusted_beats = interpolate_times(midi_object.get_beats(), midi_frame_times, audio_frame_times) # Create annotation record for the beats beat_a = jams.Annotation(namespace='beat') beat_a.annotation_metadata = annotation_metadata # Add beat timings to the annotation record for t in adjusted_beats: beat_a.append(time=t, duration=0.0) # Add beat annotation record to the JAMS file jam.annotations.append(beat_a) # Get key signature times and their string names key_change_times = [c.time for c in midi_object.key_signature_changes] key_names = [ pretty_midi.key_number_to_key_name(c.key_number) for c in midi_object.key_signature_changes ] # JAMS requires that the key name be supplied in the form e.g. # "C:major" but pretty_midi returns things in the format "C Major", # so the following code converts to JAMS format key_names = [ name.replace(' ', ':').replace('M', 'm') for name in key_names ] # Compute interpolated event times adjusted_key_change_times, adjusted_key_names = interpolate_times( key_change_times, midi_frame_times, audio_frame_times, key_names, True) # Create JAMS annotation for the key changes if len(adjusted_key_change_times) > 0: key_a = jams.Annotation(namespace='key_mode') key_a.annotation_metadata = annotation_metadata # We only have key start times from the MIDI file, but JAMS wants # durations too, so create a list of "end times" end_times = np.append(adjusted_key_change_times[1:], jam.file_metadata.duration) # Add key labels into the JAMS file for start, end, key in zip(adjusted_key_change_times, end_times, adjusted_key_names): key_a.append(time=start, duration=end - start, value=key) jam.annotations.append(key_a) return jam
def extract_ground_truth(diagnostics_group): """ Extract ground-truth information from one or more MIDI files about a single MIDI file based on the results in one or more diagnostics files and return a JAMS object with all of the annotations compiled. Parameters ---------- - diagnostics_group : list of dict List of dicts of diagnostics, each about a successful alignment of a different MIDI file to a single audio file. """ # Construct the JAMS object jam = jams.JAMS() # Load in the first diagnostics (doesn't matter which as they all # should correspond the same audio file) diagnostics = diagnostics_group[0] # Load in the audio file to get its duration for the JAMS file audio, fs = librosa.load( diagnostics['audio_filename'], feature_extraction.AUDIO_FS) jam.file_metadata.duration = librosa.get_duration(y=audio, sr=fs) # Also store metadata about the audio file, retrieved from the MSD jam.file_metadata.identifiers = {'track_id': diagnostics['audio_id']} jam.file_metadata.artist = MSD_LIST[diagnostics['audio_id']]['artist'] jam.file_metadata.title = MSD_LIST[diagnostics['audio_id']]['title'] # Iterate over the diagnostics files supplied for diagnostics in diagnostics_group: # Create annotation metadata object, shared across annotations commit = subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip() commit_url = "http://github.com/craffel/midi-dataset/tree/" + commit annotator = {'midi_md5': diagnostics['midi_md5'], 'commit_url': commit_url, 'confidence': diagnostics['score']} annotation_metadata = jams.AnnotationMetadata( curator=jams.Curator('Colin Raffel', '*****@*****.**'), version='0.0.1b', corpus='Million Song Dataset MIDI Matches', annotator=annotator, annotation_tools=( 'MIDI files were matched and aligned to audio files using the ' 'code at http://github.com/craffel/midi-dataset. Information ' 'was extracted from MIDI files using pretty_midi ' 'https://github.com/craffel/pretty-midi.'), annotation_rules=( 'Beat locations and key change times were linearly ' 'interpolated according to an audio-to-MIDI alignment.'), validation=( 'Only MIDI files with alignment confidence scores >= .5 were ' 'considered "correct". The confidence score can be used as a ' 'rough guide to the potential correctness of the annotation.'), data_source='Inferred from a MIDI file.') # Load the extracted features midi_features = deepdish.io.load(diagnostics['midi_features_filename']) audio_features = deepdish.io.load( diagnostics['audio_features_filename']) # Load in the original MIDI file midi_object = pretty_midi.PrettyMIDI(diagnostics['midi_filename']) # Compute the times of the frames (will be used for interpolation) midi_frame_times = feature_extraction.frame_times( midi_features['gram'])[diagnostics['aligned_midi_indices']] audio_frame_times = feature_extraction.frame_times( audio_features['gram'])[diagnostics['aligned_audio_indices']] # Get the interpolated beat locations and add them to the JAM adjusted_beats = interpolate_times( midi_object.get_beats(), midi_frame_times, audio_frame_times) # Create annotation record for the beats beat_a = jams.Annotation(namespace='beat') beat_a.annotation_metadata = annotation_metadata # Add beat timings to the annotation record for t in adjusted_beats: beat_a.append(time=t, duration=0.0) # Add beat annotation record to the JAMS file jam.annotations.append(beat_a) # Get key signature times and their string names key_change_times = [c.time for c in midi_object.key_signature_changes] key_names = [pretty_midi.key_number_to_key_name(c.key_number) for c in midi_object.key_signature_changes] # JAMS requires that the key name be supplied in the form e.g. # "C:major" but pretty_midi returns things in the format "C Major", # so the following code converts to JAMS format key_names = [name.replace(' ', ':').replace('M', 'm') for name in key_names] # Compute interpolated event times adjusted_key_change_times, adjusted_key_names = interpolate_times( key_change_times, midi_frame_times, audio_frame_times, key_names, True) # Create JAMS annotation for the key changes if len(adjusted_key_change_times) > 0: key_a = jams.Annotation(namespace='key_mode') key_a.annotation_metadata = annotation_metadata # We only have key start times from the MIDI file, but JAMS wants # durations too, so create a list of "end times" end_times = np.append(adjusted_key_change_times[1:], jam.file_metadata.duration) # Add key labels into the JAMS file for start, end, key in zip(adjusted_key_change_times, end_times, adjusted_key_names): key_a.append(time=start, duration=end - start, value=key) jam.annotations.append(key_a) return jam
def parse_key_number(key_number): """Parse key_number to string in pretty_midi.KeySignature. """ key = key_number_to_key_name(key_number) return generalize_key(key)
continue # pretty_midi also provides direct access to the pitch and start/end time of each note intervals = np.array( [[note.start, note.end] for note in pm.instruments[instrument_channel].notes]) notes = np.array( [note.pitch for note in pm.instruments[instrument_channel].notes]) # Get key data for each MIDI file. for key_change in pm.key_signature_changes: # print('Key {} starting at time {:.2f}'.format( # pretty_midi.key_number_to_key_name(key_change.key_number), key_change.time)) # print(pretty_midi.key_number_to_key_name(key_change.key_number)) current_chord = create_chord_vec( pretty_midi.key_number_to_key_name(key_change.key_number)) current_chord = [current_chord] * 8 # Redefine each on note as a 1 rather than assigning a velcoity. tester = piano_roll >= 1 for midi_note in range(piano_roll.shape[0]): for sixteenth_beat in range(piano_roll.shape[1]): if tester[midi_note][sixteenth_beat] == True: piano_roll[midi_note][sixteenth_beat] = 1 #If not divisible into exact number of bars, cut off the final part of bar. piano_roll_trimmed = piano_roll while piano_roll_trimmed.shape[1] / 16 % 1 != 0: #while non-integer piano_roll_trimmed = np.delete(piano_roll_trimmed, -1, axis=1) #check to avoid zero error. Not sure why piano_roll_trimmed.shape[1]==0
def midi_cleaner(data_folder, allowed_time_sigs, allowed_keys, max_time_changes=1, max_key_changes=1, ignore_filters=False): ''' ---------------------------------------------------------------------------- A function to filter a group of MIDI files by selecting only the ones that meet the specified criteria supplied for key, time signature. The files are returned as a list of pretty_midi objects. Parameters: ############################################################################ --data_folder-- The path of the folder containing the files to be filtered --allowed_time_sigs-- The time signatures to be allowed as an array of strings e.g. ['4/4'] --allowed_keys-- The key signatures to be allowed as an array of strings e.g. ['C Major', 'Bb Minor'] --max_time_changes-- The maximum number of time signature changes allowed. Default is 1. --max_key_changes-- The maximum number of key signature changes allowed. Default is 1. --ignore_filters-- If true, all MIDI files in the folder will be converted . ############################################################################ Returns: A list of pretty_midi objects meeting the supplied filter settings ---------------------------------------------------------------------------- ''' midi_files = os.listdir(data_folder) errors = 0 filtered_files = [] size, count = len(midi_files), 0.0 for num, midi_file in enumerate(midi_files): #print('Processing file {} of {}'.format(num+1,len(midi_files))) try: mid = pretty_midi.PrettyMIDI(os.path.join(data_folder, midi_file)) if not ignore_filters: time_sig_is_good, key_is_good = False, False if mid.time_signature_changes and len( mid.time_signature_changes) <= max_time_changes: time_sig_is_good = all( '{}/{}'.format(ts.numerator, ts.denominator) in allowed_time_sigs for ts in mid.time_signature_changes) if mid.key_signature_changes and len( mid.key_signature_changes) <= max_key_changes: key_is_good = all( pretty_midi.key_number_to_key_name(key.key_number) in allowed_keys for key in mid.key_signature_changes) if time_sig_is_good and key_is_good: filtered_files.append(mid) else: filtered_files.append(mid) except: errors += 1 count += 1 print('Processing files... {:4.2f}% complete. \r'.format( 100 * count / size), end='') print("") print('{} MIDI files found.'.format(len(filtered_files))) if errors: print('{} files could not be parsed.'.format(errors)) return filtered_files