# -*- coding: utf-8 -*-
"""GuitarSet Loader
GuitarSet provides audio recordings of a variety of musical excerpts
played on an acoustic guitar, along with time-aligned annotations
including pitch contours, string and fret positions, chords, beats,
downbeats, and keys.
GuitarSet contains 360 excerpts that are close to 30 seconds in length.
The 360 excerpts are the result of the following combinations:
- 6 players
- 2 versions: comping (harmonic accompaniment) and soloing (melodic improvisation)
- 5 styles: Rock, Singer-Songwriter, Bossa Nova, Jazz, and Funk
- 3 Progressions: 12 Bar Blues, Autumn Leaves, and Pachelbel Canon.
- 2 Tempi: slow and fast.
The tonality (key) of each excerpt is sampled uniformly at random.
GuitarSet was recorded with the help of a hexaphonic pickup, which outputs
signals for each string separately, allowing automated note-level annotation.
Excerpts are recorded with both the hexaphonic pickup and a Neumann U-87
condenser microphone as reference.
3 audio recordings are provided with each excerpt with the following suffix:
- hex: original 6 channel wave file from hexaphonic pickup
- hex_cln: hex wave files with interference removal applied
- mic: monophonic recording from reference microphone
- mix: monophonic mixture of original 6 channel file
Each of the 360 excerpts has an accompanying JAMS file which stores 16 annotations.
Pitch:
- 6 pitch_contour annotations (1 per string)
- 6 midi_note annotations (1 per string)
Beat and Tempo:
- 1 beat_position annotation
- 1 tempo annotation
Chords:
- 2 chord annotations: instructed and performed. The instructed chord annotation
is a digital version of the lead sheet that's provided to the player, and the
performed chord annotations are inferred from note annotations, using
segmentation and root from the digital lead sheet annotation.
For more details, please visit: http://github.com/marl/guitarset/
"""
import jams
import librosa
import logging
import numpy as np
import os
from mirdata import download_utils
from mirdata import track
from mirdata import utils
DATASET_DIR = 'GuitarSet'
REMOTES = {
'annotations': download_utils.RemoteFileMetadata(
filename='annotation.zip',
url='https://zenodo.org/record/3371780/files/annotation.zip?download=1',
checksum='b39b78e63d3446f2e54ddb7a54df9b10',
destination_dir='annotation',
),
'audio_hex_debleeded': download_utils.RemoteFileMetadata(
filename='audio_hex-pickup_debleeded.zip',
url='https://zenodo.org/record/3371780/files/audio_hex-pickup_debleeded.zip?download=1',
checksum='c31d97279464c9a67e640cb9061fb0c6',
destination_dir='audio_hex-pickup_debleeded',
),
'audio_hex_original': download_utils.RemoteFileMetadata(
filename='audio_hex-pickup_original.zip',
url='https://zenodo.org/record/3371780/files/audio_hex-pickup_original.zip?download=1',
checksum='f9911bf217cb40e9e68edf3726ef86cc',
destination_dir='audio_hex-pickup_original',
),
'audio_mic': download_utils.RemoteFileMetadata(
filename='audio_mono-mic.zip',
url='https://zenodo.org/record/3371780/files/audio_mono-mic.zip?download=1',
checksum='275966d6610ac34999b58426beb119c3',
destination_dir='audio_mono-mic',
),
'audio_mix': download_utils.RemoteFileMetadata(
filename='audio_mono-pickup_mix.zip',
url='https://zenodo.org/record/3371780/files/audio_mono-pickup_mix.zip?download=1',
checksum='aecce79f425a44e2055e46f680e10f6a',
destination_dir='audio_mono-pickup_mix',
),
}
_STYLE_DICT = {
'Jazz': 'Jazz',
'BN': 'Bossa Nova',
'Rock': 'Rock',
'SS': 'Singer-Songwriter',
'Funk': 'Funk',
}
_GUITAR_STRINGS = ['E', 'A', 'D', 'G', 'B', 'e']
DATA = utils.LargeData('guitarset_index.json')
[docs]class Track(track.Track):
"""guitarset Track class
Args:
track_id (str): track id of the track
data_home (str): Local path where the dataset is stored. default=None
If `None`, looks for the data in the default directory, `~/mir_datasets`
Attributes:
audio_hex_cln_path (str): path to the debleeded hex wave file
audio_hex_path (str): path to the original hex wave file
audio_mic_path (str): path to the mono wave via microphone
audio_mix_path (str): path to the mono wave via downmixing hex pickup
jams_path (str): path to the jams file
mode (str): one of ['solo', 'comp']
For each excerpt, players are asked to first play in 'comp' mode
and later play a 'solo' version on top of the already recorded comp.
player_id (str): ID of the different players.
one of ['00', '01', ... , '05']
style (str): one of ['Jazz', 'Bossa Nova', 'Rock', 'Singer-Songwriter', 'Funk']
tempo (float): BPM of the track
track_id (str): track id
"""
def __init__(self, track_id, data_home=None):
if track_id not in DATA.index:
raise ValueError('{} is not a valid track ID in GuitarSet'.format(track_id))
self.track_id = track_id
if data_home is None:
data_home = utils.get_default_dataset_path(DATASET_DIR)
self._data_home = data_home
self._track_paths = DATA.index[track_id]
self.audio_hex_cln_path = os.path.join(
self._data_home, self._track_paths['audio_hex_cln'][0]
)
self.audio_hex_path = os.path.join(
self._data_home, self._track_paths['audio_hex'][0]
)
self.audio_mic_path = os.path.join(
self._data_home, self._track_paths['audio_mic'][0]
)
self.audio_mix_path = os.path.join(
self._data_home, self._track_paths['audio_mix'][0]
)
self.jams_path = os.path.join(self._data_home, self._track_paths['jams'][0])
title_list = track_id.split('_') # [PID, S-T-K, mode, rec_mode]
style, tempo, _ = title_list[1].split('-') # [style, tempo, key]
self.player_id = title_list[0]
self.mode = title_list[2]
self.tempo = float(tempo)
self.style = _STYLE_DICT[style[:-1]]
@utils.cached_property
def beats(self):
"""BeatData: the track's beat positions"""
return load_beats(self.jams_path)
@utils.cached_property
def leadsheet_chords(self):
"""ChordData: the track's chords as written in the leadsheet"""
if self.mode == 'solo':
logging.info(
'Chord annotations for solo excerpts are the same with the comp excerpt.'
)
return load_chords(self.jams_path, leadsheet_version=True)
@utils.cached_property
def inferred_chords(self):
"""ChordData: the track's chords inferred from played transcription"""
if self.mode == 'solo':
logging.info(
'Chord annotations for solo excerpts are the same with the comp excerpt.'
)
return load_chords(self.jams_path, leadsheet_version=False)
@utils.cached_property
def key_mode(self):
"""KeyData: the track's key and mode"""
return load_key_mode(self.jams_path)
@utils.cached_property
def pitch_contours(self):
"""(dict): a dict that contains 6 F0Data.
From Low E string to high e string.
{
'E': F0Data(...),
'A': F0Data(...),
...
'e': F0Data(...)
}
"""
contours = {}
# iterate over 6 strings
for i in range(6):
contours[_GUITAR_STRINGS[i]] = load_pitch_contour(self.jams_path, i)
return contours
@utils.cached_property
def notes(self):
"""dict: a dict that contains 6 NoteData.
From Low E string to high e string.
{
'E': NoteData(...),
'A': NoteData(...),
...
'e': NoteData(...)
}
"""
notes = {}
# iterate over 6 strings
for i in range(6):
notes[_GUITAR_STRINGS[i]] = load_note_ann(self.jams_path, i)
return notes
@property
def audio_mic(self):
"""(np.ndarray, float): stereo microphone audio signal, sample rate"""
audio, sr = load_audio(self.audio_mic_path)
return audio, sr
@property
def audio_mix(self):
"""(np.ndarray, float): stereo mix audio signal, sample rate"""
audio, sr = load_audio(self.audio_mix_path)
return audio, sr
@property
def audio_hex(self):
"""(np.ndarray, float): raw hexaphonic audio signal, sample rate"""
audio, sr = load_multitrack_audio(self.audio_hex_path)
return audio, sr
@property
def audio_hex_cln(self):
"""(np.ndarray, float): bleed-removed hexaphonic audio signal, sample rate"""
audio, sr = load_multitrack_audio(self.audio_hex_cln_path)
return audio, sr
[docs] def to_jams(self):
"""Jams: the track's data in jams format"""
return jams.load(self.jams_path)
[docs]def load_audio(audio_path):
"""Load a Guitarset audio file.
Args:
audio_path (str): path to audio file
Returns:
y (np.ndarray): the mono audio signal
sr (float): The sample rate of the audio file
"""
if not os.path.exists(audio_path):
raise IOError("audio_path {} does not exist".format(audio_path))
return librosa.load(audio_path, sr=None, mono=True)
[docs]def load_multitrack_audio(audio_path):
"""Load a Guitarset multitrack audio file.
Args:
audio_path (str): path to audio file
Returns:
y (np.ndarray): the mono audio signal
sr (float): The sample rate of the audio file
"""
if not os.path.exists(audio_path):
raise IOError("audio_path {} does not exist".format(audio_path))
return librosa.load(audio_path, sr=None, mono=False)
[docs]def download(
data_home=None, partial_download=None, force_overwrite=False, cleanup=True
):
"""Download GuitarSet.
Args:
data_home (str):
Local path where the dataset is stored.
If `None`, looks for the data in the default directory, `~/mir_datasets`
force_overwrite (bool):
Whether to overwrite the existing downloaded data
partial_download (list):
List indicating what to partially download. The list can include any of:
* `'annotations'` the annotation files
* `'audio_hex_original'` original 6 channel wave file from hexaphonic pickup
* `'audio_hex_debleeded'` hex wave files with interference removal applied
* `'audio_mic'` monophonic recording from reference microphone
* `'audio_mix'` monophonic mixture of original 6 channel file
If `None`, all data is downloaded.
cleanup (bool):
Whether to delete the zip/tar file after extracting.
"""
if data_home is None:
data_home = utils.get_default_dataset_path(DATASET_DIR)
download_utils.downloader(
data_home,
remotes=REMOTES,
partial_download=partial_download,
info_message=None,
force_overwrite=force_overwrite,
cleanup=cleanup,
)
[docs]def validate(data_home=None, silence=False):
"""Validate if the stored dataset is a valid version
Args:
data_home (str): Local path where the dataset is stored.
If `None`, looks for the data in the default directory, `~/mir_datasets`
Returns:
missing_files (list): List of file paths that are in the dataset index
but missing locally
invalid_checksums (list): List of file paths that file exists in the dataset
index but has a different checksum compare to the reference checksum
"""
if data_home is None:
data_home = utils.get_default_dataset_path(DATASET_DIR)
missing_files, invalid_checksums = utils.validator(
DATA.index, data_home, silence=silence
)
return missing_files, invalid_checksums
[docs]def track_ids():
"""Return track ids
Returns:
(list): A list of track ids
"""
return list(DATA.index.keys())
[docs]def load(data_home=None):
"""Load GuitarSet
Args:
data_home (str): Local path where GuitarSet is stored.
If `None`, looks for the data in the default directory, `~/mir_datasets`
Returns:
(dict): {`track_id`: track data}
"""
if data_home is None:
data_home = utils.get_default_dataset_path(DATASET_DIR)
guitarset_data = {}
for key in DATA.index.keys():
guitarset_data[key] = Track(key, data_home=data_home)
return guitarset_data
def load_beats(jams_path):
if not os.path.exists(jams_path):
raise IOError("jams_path {} does not exist".format(jams_path))
jam = jams.load(jams_path)
anno = jam.search(namespace='beat_position')[0]
times, values = anno.to_event_values()
positions = [int(v['position']) for v in values]
return utils.BeatData(times, positions)
[docs]def load_chords(jams_path, leadsheet_version=True):
"""
Args:
jams_path (str): Path of the jams annotation file
leadsheet_version (Bool)
Whether or not to load the leadsheet version of the chord annotation
If False, load the infered version.
Returns:
(ChordData): Chord data
"""
if not os.path.exists(jams_path):
raise IOError("jams_path {} does not exist".format(jams_path))
jam = jams.load(jams_path)
if leadsheet_version:
anno = jam.search(namespace='chord')[0]
else:
anno = jam.search(namespace='chord')[1]
intervals, values = anno.to_interval_values()
return utils.ChordData(intervals, values)
def load_key_mode(jams_path):
if not os.path.exists(jams_path):
raise IOError("jams_path {} does not exist".format(jams_path))
jam = jams.load(jams_path)
anno = jam.search(namespace='key_mode')[0]
intervals, values = anno.to_interval_values()
return utils.KeyData(intervals[:, 0], intervals[:, 1], values)
[docs]def load_pitch_contour(jams_path, string_num):
"""
Args:
jams_path (str): Path of the jams annotation file
string_num (int), in range(6): Which string to load.
0 is the Low E string, 5 is the high e string.
"""
if not os.path.exists(jams_path):
raise IOError("jams_path {} does not exist".format(jams_path))
jam = jams.load(jams_path)
anno_arr = jam.search(namespace='pitch_contour')
anno = anno_arr.search(data_source=str(string_num))[0]
times, values = anno.to_event_values()
frequencies = [v['frequency'] for v in values]
return utils.F0Data(times, frequencies, np.ones_like(times))
[docs]def load_note_ann(jams_path, string_num):
"""
Args:
jams_path (str): Path of the jams annotation file
string_num (int), in range(6): Which string to load.
0 is the Low E string, 5 is the high e string.
"""
if not os.path.exists(jams_path):
raise IOError("jams_path {} does not exist".format(jams_path))
jam = jams.load(jams_path)
anno_arr = jam.search(namespace='note_midi')
anno = anno_arr.search(data_source=str(string_num))[0]
intervals, values = anno.to_interval_values()
return utils.NoteData(intervals, values, np.ones_like(values))
[docs]def cite():
"""Print the reference"""
cite_data = """
=========== MLA ===========
Xi, Qingyang, et al.
"GuitarSet: A Dataset for Guitar Transcription."
In Proceedings of the 19th International Society for Music Information Retrieval Conference (ISMIR). 2018.
========== Bibtex ==========
@inproceedings{xi2018guitarset,
title={GuitarSet: A Dataset for Guitar Transcription},
author={Xi, Qingyang and Bittner, Rachel M and Ye, Xuzhou and Pauwels, Johan and Bello, Juan P},
booktitle={International Society of Music Information Retrieval (ISMIR)},
year={2018}
}
"""
print(cite_data)