codecov[bot]: # [Codecov](https://codecov.io/gh/mir-dataset-loaders/mirdata/pull/72?src=pr&el=h1) Report
> Merging [#72](https://codecov.io/gh/mir-dataset-loaders/mirdata/pull/72?src=pr&el=desc) into [master](https://codecov.io/gh/mir-dataset-loaders/mirdata/commit/d324391d7eac346b0599c26252daec7eeafa481b?src=pr&el=desc) will **decrease** coverage by `4.29%`.
> The diff coverage is `34.56%`.


```diff
@@            Coverage Diff            @@
##           master      #72     +/-   ##
=========================================
- Coverage    50.2%   45.91%   -4.3%     
=========================================
  Files           9        9             
  Lines         723      771     +48     
=========================================
- Hits          363      354      -9     
- Misses        360      417     +57
```

diff --git a/mirdata/beatles.py b/mirdata/beatles.py
index 734def9..1667995 100644
--- a/mirdata/beatles.py
+++ b/mirdata/beatles.py
@@ -1,39 +1,70 @@
 """Beatles Dataset Loader
 """
-from collections import namedtuple
 import numpy as np
 import os
 import csv
-import json
 
 import mirdata.utils as utils
 
-BEATLES_DIR = 'Beatles'
-BEATLES_INDEX = utils.load_json_index('beatles_index.json')
-BEATLES_ANNOT_REMOTE = utils.RemoteFileMetadata(
+DATASET_DIR = 'Beatles'
+INDEX = utils.load_json_index('beatles_index.json')
+ANNOTATIONS_REMOTE = utils.RemoteFileMetadata(
     filename='The Beatles Annotations.tar.gz',
-    url='http://isophonics.net/files/annotations/' 'The%20Beatles%20Annotations.tar.gz',
+    url='http://isophonics.net/files/annotations/The%20Beatles%20Annotations.tar.gz',
     checksum='62425c552d37c6bb655a78e4603828cc',
 )
 
-BeatlesTrack = namedtuple(
-    'BeatlesTrack',
-    ['track_id', 'audio_path', 'beats', 'chords', 'key', 'sections', 'title'],
-)
+
+class Track(object):
+    def __init__(self, track_id, data_home=None):
+        if track_id not in INDEX:
+            raise ValueError(
+                '{} is not a valid track ID in Beatles'.format(track_id))
+
+        self.track_id = track_id
+
+        self._data_home = data_home
+        self._track_paths = INDEX[track_id]
+
+        self.audio_path = utils.get_local_path(
+            self._data_home, self._track_paths['audio'][0])
+
+        self.title = os.path.basename(
+            self._track_paths['sections'][0]).split('.')[0]
+
+    @utils.cached_property
+    def beats(self):
+        return _load_beats(utils.get_local_path(
+            self._data_home, self._track_paths['beat'][0]))
+
+    @utils.cached_property
+    def chords(self):
+        return _load_chords(utils.get_local_path(
+            self._data_home, self._track_paths['chords'][0]))
+
+    @utils.cached_property
+    def key(self):
+        return _load_key(utils.get_local_path(
+            self._data_home, self._track_paths['keys'][0]))
+
+    @utils.cached_property
+    def sections(self):
+        return _load_sections(utils.get_local_path(
+            self._data_home, self._track_paths['sections'][0]))
 
 
 def download(data_home=None, force_overwrite=False):
     save_path = utils.get_save_path(data_home)
-    dataset_path = os.path.join(save_path, BEATLES_DIR)
+    dataset_path = os.path.join(save_path, DATASET_DIR)
 
     if exists(data_home) and not force_overwrite:
         return
 
     if force_overwrite:
-        utils.force_delete_all(BEATLES_ANNOT_REMOTE, dataset_path=None, data_home=data_home)
+        utils.force_delete_all(ANNOTATIONS_REMOTE, dataset_path=None, data_home=data_home)
 
     download_path = utils.download_from_remote(
-        BEATLES_ANNOT_REMOTE, data_home=data_home, force_overwrite=force_overwrite
+        ANNOTATIONS_REMOTE, data_home=data_home, force_overwrite=force_overwrite
     )
     if not os.path.exists(dataset_path):
         os.makedirs(dataset_path)
@@ -57,56 +88,32 @@ def download(data_home=None, force_overwrite=False):
 
 def exists(data_home=None):
     save_path = utils.get_save_path(data_home)
-    dataset_path = os.path.join(save_path, BEATLES_DIR)
+    dataset_path = os.path.join(save_path, DATASET_DIR)
     return os.path.exists(dataset_path)
 
 
 def validate(dataset_path, data_home=None):
     missing_files, invalid_checksums = utils.validator(
-        BEATLES_INDEX, data_home, dataset_path
+        INDEX, data_home, dataset_path
     )
     return missing_files, invalid_checksums
 
 
 def track_ids():
-    return list(BEATLES_INDEX.keys())
+    return list(INDEX.keys())
 
 
 def load(data_home=None):
     save_path = utils.get_save_path(data_home)
-    dataset_path = os.path.join(save_path, BEATLES_DIR)
+    dataset_path = os.path.join(save_path, DATASET_DIR)
 
     validate(dataset_path, data_home)
     beatles_data = {}
     for key in track_ids():
-        beatles_data[key] = load_track(key, data_home=data_home)
+        beatles_data[key] = Track(key, data_home=data_home)
     return beatles_data
 
 
-def load_track(track_id, data_home=None):
-    if track_id not in BEATLES_INDEX.keys():
-        raise ValueError('{} is not a valid track ID in Beatles'.format(track_id))
-
-    track_data = BEATLES_INDEX[track_id]
-
-    beat_data = _load_beats(utils.get_local_path(data_home, track_data['beat'][0]))
-    chord_data = _load_chords(utils.get_local_path(data_home, track_data['chords'][0]))
-    key_data = _load_key(utils.get_local_path(data_home, track_data['keys'][0]))
-    section_data = _load_sections(
-        utils.get_local_path(data_home, track_data['sections'][0])
-    )
-
-    return BeatlesTrack(
-        track_id,
-        utils.get_local_path(data_home, track_data['audio'][0]),
-        beat_data,
-        chord_data,
-        key_data,
-        section_data,
-        os.path.basename(track_data['sections'][0]).split('.')[0],
-    )
-
-
 def _load_beats(beats_path):
     if beats_path is None or not os.path.exists(beats_path):
         return None
diff --git a/mirdata/ikala.py b/mirdata/ikala.py
index 26fd497..e625979 100644
--- a/mirdata/ikala.py
+++ b/mirdata/ikala.py
@@ -6,8 +6,6 @@ from __future__ import print_function
 
 """ikala dataset loader
 """
-from collections import namedtuple
-
 import csv
 import os
 import librosa
@@ -15,17 +13,41 @@ import numpy as np
 
 import mirdata.utils as utils
 
-IKALA_TIME_STEP = 0.032  # seconds
-IKALA_INDEX = utils.load_json_index('ikala_index.json')
-IKALA_METADATA = None
-IKALA_DIR = 'iKala'
+TIME_STEP = 0.032  # seconds
+INDEX = utils.load_json_index('ikala_index.json')
+METADATA = None
+DATASET_DIR = 'iKala'
 ID_MAPPING_URL = 'http://mac.citi.sinica.edu.tw/ikala/id_mapping.txt'
 
 
-IKalaTrack = namedtuple(
-    'IKalaTrack',
-    ['track_id', 'f0', 'lyrics', 'audio_path', 'singer_id', 'song_id', 'section'],
-)
+class Track(object):
+    def __init__(self, track_id, data_home=None):
+        if track_id not in INDEX:
+            raise ValueError(
+                '{} is not a valid track ID in iKala'.format(track_id))
+
+        if METADATA is None or METADATA['data_home'] != data_home:
+            _reload_metadata(data_home)
+
+        self.track_id = track_id
+        self._data_home = data_home
+        self._track_paths = INDEX[track_id]
+
+        self.audio_path = utils.get_local_path(
+            self._data_home, self._track_paths['audio'][0])
+        self.song_id = track_id.split('_')[0]
+        self.section = track_id.split('_')[0]
+        self.singer_id = METADATA[self.song_id]
+
+    @utils.cached_property
+    def f0(self):
+        return _load_f0(utils.get_local_path(
+            self._data_home, self._track_paths['pitch'][0]))
+
+    @utils.cached_property
+    def lyrics(self):
+        return _load_lyrics(utils.get_local_path(
+            self._data_home, self._track_paths['lyrics'][0]))
 
 
 def download(data_home=None):
@@ -42,55 +64,30 @@ def download(data_home=None):
                 > Wavfile/
         and copy the {ikala_dir} folder to {save_path}
     """.format(
-            ikala_dir=IKALA_DIR, save_path=save_path
+            ikala_dir=DATASET_DIR, save_path=save_path
         )
     )
 
 
 def validate(dataset_path, data_home=None):
     missing_files, invalid_checksums = utils.validator(
-        IKALA_INDEX, data_home, dataset_path
+        INDEX, data_home, dataset_path
     )
     return missing_files, invalid_checksums
 
 
 def track_ids():
-    return list(IKALA_INDEX.keys())
+    return list(INDEX.keys())
 
 
 def load(data_home=None):
     validate(data_home)
     ikala_data = {}
-    for key in IKALA_INDEX.keys():
-        ikala_data[key] = load_track(key, data_home=data_home)
+    for key in INDEX.keys():
+        ikala_data[key] = Track(key, data_home=data_home)
     return ikala_data
 
 
-def load_track(track_id, data_home=None):
-    if track_id not in IKALA_INDEX.keys():
-        raise ValueError('{} is not a valid track ID in IKala'.format(track_id))
-
-    if IKALA_METADATA is None or IKALA_METADATA['data_home'] != data_home:
-        _reload_metadata(data_home)
-
-    track_data = IKALA_INDEX[track_id]
-    f0_data = _load_f0(utils.get_local_path(data_home, track_data['pitch'][0]))
-    lyrics_data = _load_lyrics(utils.get_local_path(data_home, track_data['lyrics'][0]))
-
-    song_id = track_id.split('_')[0]
-    section = track_id.split('_')[1]
-
-    return IKalaTrack(
-        track_id,
-        f0_data,
-        lyrics_data,
-        utils.get_local_path(data_home, track_data['audio'][0]),
-        IKALA_METADATA[song_id],
-        song_id,
-        section,
-    )
-
-
 def load_ikala_vocal_audio(ikalatrack):
     audio_path = ikalatrack.audio_path
     audio, sr = librosa.load(audio_path, sr=None, mono=False)
@@ -108,7 +105,7 @@ def load_ikala_instrumental_audio(ikalatrack):
 def load_ikala_mix_audio(ikalatrack):
     audio_path = ikalatrack.audio_path
     mixed_audio, sr = librosa.load(audio_path, sr=None, mono=True)
-    return mixed_audio, sr
+    return 2.0 * mixed_audio, sr
 
 
 def _load_f0(f0_path):
@@ -120,7 +117,7 @@ def _load_f0(f0_path):
     f0_midi = np.array([float(line) for line in lines])
     f0_hz = librosa.midi_to_hz(f0_midi) * (f0_midi > 0)
     confidence = (f0_hz > 0).astype(int)
-    times = np.arange(len(f0_midi)) * IKALA_TIME_STEP
+    times = (np.arange(len(f0_midi)) * TIME_STEP) + (TIME_STEP / 2.0)
     f0_data = utils.F0Data(times, f0_hz, confidence)
     return f0_data
 
@@ -128,35 +125,35 @@ def _load_f0(f0_path):
 def _load_lyrics(lyrics_path):
     if not os.path.exists(lyrics_path):
         return None
-    # input: start time (ms), end time (ms), lyric, [pronounciation]
+    # input: start time (ms), end time (ms), lyric, [pronunciation]
     with open(lyrics_path, 'r') as fhandle:
         reader = csv.reader(fhandle, delimiter=' ')
         start_times = []
         end_times = []
         lyrics = []
-        pronounciations = []
+        pronunciations = []
         for line in reader:
             start_times.append(float(line[0]) / 1000.0)
             end_times.append(float(line[1]) / 1000.0)
             lyrics.append(line[2])
             if len(line) > 2:
-                pronounciation = ' '.join(line[3:])
-                pronounciations.append(pronounciation if pronounciation != '' else None)
+                pronunciation = ' '.join(line[3:])
+                pronunciations.append(pronunciation if pronunciation != '' else None)
             else:
-                pronounciations.append(None)
+                pronunciations.append(None)
 
-    lyrics_data = utils.LyricsData(start_times, end_times, lyrics, pronounciations)
+    lyrics_data = utils.LyricData(start_times, end_times, lyrics, pronunciations)
     return lyrics_data
 
 
 def _reload_metadata(data_home):
-    global IKALA_METADATA
-    IKALA_METADATA = _load_metadata(data_home=data_home)
+    global METADATA
+    METADATA = _load_metadata(data_home=data_home)
 
 
 def _load_metadata(data_home):
     id_map_path = utils.get_local_path(
-        data_home, os.path.join(IKALA_DIR, 'id_mapping.txt')
+        data_home, os.path.join(DATASET_DIR, 'id_mapping.txt')
     )
     if not os.path.exists(id_map_path):
         utils.download_large_file(ID_MAPPING_URL, id_map_path)
diff --git a/mirdata/maps.py b/mirdata/maps.py
index c8d2eb7..cbace67 100644
--- a/mirdata/maps.py
+++ b/mirdata/maps.py
@@ -1,18 +1,18 @@
 from .utils import download_from_remote, untar, RemoteFileMetadata, get_save_path
 
 
-MAPS_META = RemoteFileMetadata(
+REMOTE = RemoteFileMetadata(
     filename='MAPS.tar',
     url='https://amubox.univ-amu.fr/s/iNG0xc5Td1Nv4rR/download',
     checksum=('9a8b89a7897b0ad95a505b4daa788302'),
 )
 
-MAPS_DIR = 'MAPS'
+DATASET_DIR = 'MAPS'
 
 
 def download(data_home=None, force_overwrite=False):
     save_path = get_save_path(data_home)
-    download_path = download_from_remote(MAPS_META, force_overwrite=force_overwrite)
+    download_path = download_from_remote(REMOTE, force_overwrite=force_overwrite)
     untar(download_path, save_path)
     validate()
 
diff --git a/mirdata/medleydb_melody.py b/mirdata/medleydb_melody.py
index 126729c..6be79b7 100644
--- a/mirdata/medleydb_melody.py
+++ b/mirdata/medleydb_melody.py
@@ -6,7 +6,6 @@ from __future__ import print_function
 
 """Orchset Dataset Loader
 """
-from collections import namedtuple
 import csv
 import json
 import numpy as np
@@ -14,26 +13,50 @@ import os
 
 import mirdata.utils as utils
 
-MEDLEYDB_MELODY_INDEX = utils.load_json_index('medleydb_melody_index.json')
-MEDLEYDB_MELODY_DIR = 'MedleyDB-Melody'
-MEDLEYDB_METADATA = None
-
-MedleydbMelodyTrack = namedtuple(
-    'MedleydbMelodyTrack',
-    [
-        'track_id',
-        'melody1',
-        'melody2',
-        'melody3',
-        'audio_path',
-        'artist',
-        'title',
-        'genre',
-        'is_excerpt',
-        'is_instrumental',
-        'n_sources',
-    ],
-)
+INDEX = utils.load_json_index('medleydb_melody_index.json')
+DATASET_DIR = 'MedleyDB-Melody'
+METADATA = None
+
+
+class Track(object):
+    def __init__(self, track_id, data_home=None):
+        if track_id not in INDEX:
+            raise ValueError(
+                '{} is not a valid track ID in MedleyDB-Melody'.format(track_id)
+            )
+
+        self.track_id = track_id
+        self._data_home = data_home
+        self._track_paths = INDEX[track_id]
+
+        if METADATA is None or METADATA['data_home'] != data_home:
+            _reload_metadata(data_home)
+
+        self._track_metadata = METADATA[track_id]
+
+        self.audio_path = utils.get_local_path(
+            self._data_home, self._track_paths['audio'][0])
+        self.artist = self._track_metadata['artist']
+        self.title = self._track_metadata['title']
+        self.genre = self._track_metadata['genre']
+        self.is_excerpt = self._track_metadata['is_excerpt']
+        self.is_instrumental = self._track_metadata['is_instrumental']
+        self.n_sources = self._track_metadata['n_sources']
+
+    @utils.cached_property
+    def melody1(self):
+        return _load_melody(utils.get_local_path(
+            self._data_home, self._track_paths['melody1'][0]))
+
+    @utils.cached_property
+    def melody2(self):
+        return _load_melody(utils.get_local_path(
+            self._data_home, self._track_paths['melody2'][0]))
+
+    @utils.cached_property
+    def melody3(self):
+        return _load_melody3(utils.get_local_path(
+            self._data_home, self._track_paths['melody3'][0]))
 
 
 def download(data_home=None):
@@ -56,65 +79,26 @@ def download(data_home=None):
 
 def validate(dataset_path, data_home=None):
     missing_files, invalid_checksums = utils.validator(
-        MEDLEYDB_MELODY_INDEX, data_home, dataset_path
+        INDEX, data_home, dataset_path
     )
     return missing_files, invalid_checksums
 
 
 def track_ids():
-    return list(MEDLEYDB_MELODY_INDEX.keys())
+    return list(INDEX.keys())
 
 
 def load(data_home=None):
     save_path = utils.get_save_path(data_home)
-    dataset_path = os.path.join(save_path, MEDLEYDB_MELODY_DIR)
+    dataset_path = os.path.join(save_path, DATASET_DIR)
 
     validate(dataset_path, data_home)
     medleydb_melody_data = {}
     for key in track_ids():
-        medleydb_melody_data[key] = load_track(key, data_home=data_home)
+        medleydb_melody_data[key] = Track(key, data_home=data_home)
     return medleydb_melody_data
 
 
-def load_track(track_id, data_home=None):
-    if track_id not in MEDLEYDB_MELODY_INDEX.keys():
-        raise ValueError(
-            '{} is not a valid track ID in MedleyDB-Melody'.format(track_id)
-        )
-    track_data = MEDLEYDB_MELODY_INDEX[track_id]
-
-    if MEDLEYDB_METADATA is None or MEDLEYDB_METADATA['data_home'] != data_home:
-        _reload_metadata(data_home)
-        if MEDLEYDB_METADATA is None:
-            raise EnvironmentError('Could not find MedleyDB-Melody metadata file')
-
-    track_metadata = MEDLEYDB_METADATA[track_id]
-
-    melody1_data = _load_melody(
-        utils.get_local_path(data_home, track_data['melody1'][0])
-    )
-    melody2_data = _load_melody(
-        utils.get_local_path(data_home, track_data['melody2'][0])
-    )
-    melody3_data = _load_melody3(
-        utils.get_local_path(data_home, track_data['melody3'][0])
-    )
-
-    return MedleydbMelodyTrack(
-        track_id,
-        melody1_data,
-        melody2_data,
-        melody3_data,
-        utils.get_local_path(data_home, track_data['audio'][0]),
-        track_metadata['artist'],
-        track_metadata['title'],
-        track_metadata['genre'],
-        track_metadata['is_excerpt'],
-        track_metadata['is_instrumental'],
-        track_metadata['n_sources'],
-    )
-
-
 def _load_melody(melody_path):
     if not os.path.exists(melody_path):
         return None
@@ -150,16 +134,16 @@ def _load_melody3(melody_path):
 
 
 def _reload_metadata(data_home):
-    global MEDLEYDB_METADATA
-    MEDLEYDB_METADATA = _load_metadata(data_home=data_home)
+    global METADATA
+    METADATA = _load_metadata(data_home=data_home)
 
 
 def _load_metadata(data_home):
     metadata_path = utils.get_local_path(
-        data_home, os.path.join(MEDLEYDB_MELODY_DIR, 'medleydb_melody_metadata.json')
+        data_home, os.path.join(DATASET_DIR, 'medleydb_melody_metadata.json')
     )
     if not os.path.exists(metadata_path):
-        return None
+        raise OSError('Could not find MedleyDB-Melody metadata file')
     with open(metadata_path, 'r') as fhandle:
         metadata = json.load(fhandle)
 
diff --git a/mirdata/medleydb_pitch.py b/mirdata/medleydb_pitch.py
index eb6e269..c4d2342 100644
--- a/mirdata/medleydb_pitch.py
+++ b/mirdata/medleydb_pitch.py
@@ -6,7 +6,6 @@ from __future__ import print_function
 
 """Orchset Dataset Loader
 """
-from collections import namedtuple
 import csv
 import json
 import numpy as np
@@ -14,14 +13,38 @@ import os
 
 import mirdata.utils as utils
 
-MEDLEYDB_PITCH_INDEX = utils.load_json_index('medleydb_pitch_index.json')
-MEDLEYDB_PITCH_DIR = 'MedleyDB-Pitch'
-MEDLEYDB_METADATA = None
+INDEX = utils.load_json_index('medleydb_pitch_index.json')
+DATASET_DIR = 'MedleyDB-Pitch'
+METADATA = None
 
-MedleydbPitchTrack = namedtuple(
-    'MedleydbPitchTrack',
-    ['track_id', 'pitch', 'audio_path', 'instrument', 'artist', 'title', 'genre'],
-)
+
+class Track(object):
+    def __init__(self, track_id, data_home=None):
+        if track_id not in INDEX:
+            raise ValueError(
+                '{} is not a valid track ID in MedleyDB-Pitch'.format(track_id)
+            )
+
+        self.track_id = track_id
+        self._data_home = data_home
+        self._track_paths = INDEX[track_id]
+
+        if METADATA is None or METADATA['data_home'] != data_home:
+            _reload_metadata(data_home)
+
+        self._track_metadata = METADATA[track_id]
+
+        self.audio_path = utils.get_local_path(
+            self._data_home, self._track_paths['audio'][0])
+        self.instrument = self._track_metadata['instrument']
+        self.artist = self._track_metadata['artist']
+        self.title = self._track_metadata['title']
+        self.genre = self._track_metadata['genre']
+
+    @utils.cached_property
+    def pitch(self):
+        return _load_pitch(utils.get_local_path(
+            self._data_home, self._track_paths['pitch'][0]))
 
 
 def download(data_home=None):
@@ -44,18 +67,18 @@ def download(data_home=None):
 
 def validate(dataset_path, data_home=None):
     missing_files, invalid_checksums = utils.validator(
-        MEDLEYDB_PITCH_INDEX, data_home, dataset_path
+        INDEX, data_home, dataset_path
     )
     return missing_files, invalid_checksums
 
 
 def track_ids():
-    return list(MEDLEYDB_PITCH_INDEX.keys())
+    return list(INDEX.keys())
 
 
 def load(data_home=None):
     save_path = utils.get_save_path(data_home)
-    dataset_path = os.path.join(save_path, MEDLEYDB_PITCH_DIR)
+    dataset_path = os.path.join(save_path, DATASET_DIR)
 
     validate(dataset_path, data_home)
     medleydb_pitch_data = {}
@@ -64,33 +87,6 @@ def load(data_home=None):
     return medleydb_pitch_data
 
 
-def load_track(track_id, data_home=None):
-    if track_id not in MEDLEYDB_PITCH_INDEX.keys():
-        raise ValueError(
-            '{} is not a valid track ID in MedleyDB-Pitch'.format(track_id)
-        )
-    track_data = MEDLEYDB_PITCH_INDEX[track_id]
-
-    if MEDLEYDB_METADATA is None or MEDLEYDB_METADATA['data_home'] != data_home:
-        _reload_metadata(data_home)
-        if MEDLEYDB_METADATA is None:
-            raise EnvironmentError('Could not find MedleyDB-Pitch metadata file')
-
-    track_metadata = MEDLEYDB_METADATA[track_id]
-
-    pitch_data = _load_pitch(utils.get_local_path(data_home, track_data['pitch'][0]))
-
-    return MedleydbPitchTrack(
-        track_id,
-        pitch_data,
-        utils.get_local_path(data_home, track_data['audio'][0]),
-        track_metadata['instrument'],
-        track_metadata['artist'],
-        track_metadata['title'],
-        track_metadata['genre'],
-    )
-
-
 def _load_pitch(pitch_path):
     if not os.path.exists(pitch_path):
         return None
@@ -109,16 +105,16 @@ def _load_pitch(pitch_path):
 
 
 def _reload_metadata(data_home):
-    global MEDLEYDB_METADATA
-    MEDLEYDB_METADATA = _load_metadata(data_home=data_home)
+    global METADATA
+    METADATA = _load_metadata(data_home=data_home)
 
 
 def _load_metadata(data_home):
     metadata_path = utils.get_local_path(
-        data_home, os.path.join(MEDLEYDB_PITCH_DIR, 'medleydb_pitch_metadata.json')
+        data_home, os.path.join(DATASET_DIR, 'medleydb_pitch_metadata.json')
     )
     if not os.path.exists(metadata_path):
-        return None
+        raise OSError('Could not find MedleyDB-Pitch metadata file')
     with open(metadata_path, 'r') as fhandle:
         metadata = json.load(fhandle)
 
diff --git a/mirdata/orchset.py b/mirdata/orchset.py
index 89150cb..9385b38 100644
--- a/mirdata/orchset.py
+++ b/mirdata/orchset.py
@@ -6,123 +6,105 @@ from __future__ import print_function
 
 """Orchset Dataset Loader
 """
-from collections import namedtuple
 import csv
 import numpy as np
 import os
 
 import mirdata.utils as utils
 
-ORCHSET_INDEX = utils.load_json_index('orchset_index.json')
+INDEX = utils.load_json_index('orchset_index.json')
 
-ORCHSET_META = utils.RemoteFileMetadata(
+REMOTE = utils.RemoteFileMetadata(
     filename='Orchset_dataset_0.zip',
-    url='https://zenodo.org/record/1289786/files/' 'Orchset_dataset_0.zip?download=1',
+    url='https://zenodo.org/record/1289786/files/Orchset_dataset_0.zip?download=1',
     checksum=('cf6fe52d64624f61ee116c752fb318ca'),
 )
 
-ORCHSET_DIR = 'Orchset'
-ORCHSET_METADATA = None
-
-OrchsetTrack = namedtuple(
-    'OrchsetTrack',
-    [
-        'track_id',
-        'melody',
-        'audio_path_mono',
-        'audio_path_stereo',
-        'composer',
-        'work',
-        'excerpt',
-        'predominant_melodic_instruments',
-        'alternating_melody',
-        'contains_winds',
-        'contains_strings',
-        'contains_brass',
-        'only_strings',
-        'only_winds',
-        'only_brass',
-    ],
-)
+DATASET_DIR = 'Orchset'
+METADATA = None
+
+
+class Track(object):
+    def __init__(self, track_id, data_home=None):
+        if track_id not in INDEX:
+            raise ValueError('{} is not a valid track ID in Orchset'.format(track_id))
+
+        self.track_id = track_id
+        self._data_home = data_home
+        self._track_paths = INDEX[track_id]
+
+        if METADATA is None or METADATA['data_home'] != data_home:
+            _reload_metadata(data_home)
+
+        self._track_metadata = METADATA[track_id]
+
+        self.audio_path_mono = utils.get_local_path(
+            self._data_home, self._track_paths['audio_mono'][0])
+        self.audio_path_stereo = utils.get_local_path(
+            self._data_home, self._track_paths['audio_stereo'][0])
+        self.composer = self._track_metadata['composer']
+        self.work = self._track_metadata['work']
+        self.excerpt = self._track_metadata['excerpt']
+        self.predominant_melodic_instruments = \
+            self._track_metadata['predominant_melodic_instruments-normalized']
+        self.alternating_melody = self._track_metadata['alternating_melody']
+        self.contains_winds = self._track_metadata['contains_winds']
+        self.contains_strings = self._track_metadata['contains_strings']
+        self.contains_brass = self._track_metadata['contains_brass']
+        self.only_strings = self._track_metadata['only_strings']
+        self.only_winds = self._track_metadata['only_winds']
+        self.only_brass = self._track_metadata['only_brass']
+
+    @utils.cached_property
+    def melody(self):
+        return _load_melody(utils.get_local_path(
+            self._data_home, self._track_paths['melody'][0]))
 
 
 def download(data_home=None, force_overwrite=False):
     save_path = utils.get_save_path(data_home)
-    dataset_path = os.path.join(save_path, ORCHSET_DIR)
+    dataset_path = os.path.join(save_path, DATASET_DIR)
 
     if exists(data_home) and not force_overwrite:
         return
 
     if force_overwrite:
-        utils.force_delete_all(ORCHSET_META, dataset_path, data_home)
+        utils.force_delete_all(REMOTE, dataset_path, data_home)
 
     download_path = utils.download_from_remote(
-        ORCHSET_META, force_overwrite=force_overwrite
+        REMOTE, force_overwrite=force_overwrite
     )
     utils.unzip(download_path, save_path, dataset_path)
 
 
 def exists(data_home=None):
     save_path = utils.get_save_path(data_home)
-    dataset_path = os.path.join(save_path, ORCHSET_DIR)
+    dataset_path = os.path.join(save_path, DATASET_DIR)
     return os.path.exists(dataset_path)
 
 
 def validate(dataset_path, data_home=None):
     missing_files, invalid_checksums = utils.validator(
-        ORCHSET_INDEX, data_home, dataset_path
+        INDEX, data_home, dataset_path
     )
     return missing_files, invalid_checksums
 
 
 def track_ids():
-    return list(ORCHSET_INDEX.keys())
+    return list(INDEX.keys())
 
 
 def load(data_home=None):
     save_path = utils.get_save_path(data_home)
-    dataset_path = os.path.join(save_path, ORCHSET_DIR)
+    dataset_path = os.path.join(save_path, DATASET_DIR)
 
     validate(dataset_path, data_home)
     orchset_data = {}
     for key in track_ids():
-        orchset_data[key] = load_track(key, data_home=data_home)
+        orchset_data[key] = Track(key, data_home=data_home)
     return orchset_data
 
 
-def load_track(track_id, data_home=None):
-    if track_id not in ORCHSET_INDEX.keys():
-        raise ValueError('{} is not a valid track ID in Orchset'.format(track_id))
-    track_data = ORCHSET_INDEX[track_id]
-
-    if ORCHSET_METADATA is None or ORCHSET_METADATA['data_home'] != data_home:
-        _reload_metadata(data_home)
-        if ORCHSET_METADATA is None:
-            raise EnvironmentError('Could not find Orchset metadata file')
-
-    melody_data = _load_melody(utils.get_local_path(data_home, track_data['melody'][0]))
-
-    track_metadata = ORCHSET_METADATA[track_id]
-
-    return OrchsetTrack(
-        track_id,
-        melody_data,
-        utils.get_local_path(data_home, track_data['audio_mono'][0]),
-        utils.get_local_path(data_home, track_data['audio_stereo'][0]),
-        track_metadata['composer'],
-        track_metadata['work'],
-        track_metadata['excerpt'],
-        track_metadata['predominant_melodic_instruments-normalized'],
-        track_metadata['alternating_melody'],
-        track_metadata['contains_winds'],
-        track_metadata['contains_strings'],
-        track_metadata['contains_brass'],
-        track_metadata['only_strings'],
-        track_metadata['only_winds'],
-        track_metadata['only_brass'],
-    )
-
-
 def _load_melody(melody_path):
     if not os.path.exists(melody_path):
         return None
@@ -145,11 +127,11 @@ def _load_metadata(data_home):
 
     predominant_inst_path = utils.get_local_path(
         data_home,
-        os.path.join(ORCHSET_DIR, 'Orchset - Predominant Melodic Instruments.csv'),
+        os.path.join(DATASET_DIR, 'Orchset - Predominant Melodic Instruments.csv'),
     )
 
     if not os.path.exists(predominant_inst_path):
-        return None
+        raise OSError('Could not find Orchset metadata file')
 
     with open(predominant_inst_path, 'r') as fhandle:
         reader = csv.reader(fhandle, delimiter=',')
@@ -202,8 +184,8 @@ def _load_metadata(data_home):
 
 
 def _reload_metadata(data_home):
-    global ORCHSET_METADATA
-    ORCHSET_METADATA = _load_metadata(data_home=data_home)
+    global METADATA
+    METADATA = _load_metadata(data_home=data_home)
 
 
 def cite():
diff --git a/mirdata/salami.py b/mirdata/salami.py
index 666574e..b58c05f 100644
--- a/mirdata/salami.py
+++ b/mirdata/salami.py
@@ -1,56 +1,97 @@
 """Salami Dataset Loader
 """
-from collections import namedtuple
 import csv
 import numpy as np
 import os
 
 import mirdata.utils as utils
 
-SALAMI_INDEX = utils.load_json_index('salami_index.json')
-SALAMI_METADATA = None
-SALAMI_DIR = 'Salami'
-SALAMI_ANNOT_REMOTE = utils.RemoteFileMetadata(
+INDEX = utils.load_json_index('salami_index.json')
+METADATA = None
+DATASET_DIR = 'Salami'
+ANNOTATIONS_REMOTE = utils.RemoteFileMetadata(
     filename='salami-data-public-master.zip',
     url='https://github.com/DDMAL/salami-data-public/archive/master.zip',
     checksum='b01d6eb5b71cca1f3163fae4b2cd4c61',
 )
 
-SalamiTrack = namedtuple(
-    'SalamiTrack',
-    [
-        'track_id',
-        'audio_path',
-        'sections_annotator_1_uppercase',
-        'sections_annotator_1_lowercase',
-        'sections_annotator_2_uppercase',
-        'sections_annotator_2_lowercase',
-        'source',
-        'annotator_1_id',
-        'annotator_2_id',
-        'duration_sec',
-        'title',
-        'artist',
-        'annotator_1_time',
-        'annotator_2_time',
-        'broad_genre',
-        'genre',
-    ],
-)
+
+class Track(object):
+    def __init__(self, track_id, data_home=None):
+        if track_id not in INDEX:
+            raise ValueError('{} is not a valid track ID in Salami'.format(track_id))
+
+        self.track_id = track_id
+        self._data_home = data_home
+        self._track_paths = INDEX[track_id]
+
+        if METADATA is None or METADATA['data_home'] != data_home:
+            _reload_metadata(data_home)
+
+        if track_id in METADATA.keys():
+            self._track_metadata = METADATA[track_id]
+        else:
+            # annotations with missing metadata
+            self._track_metadata = {
+                'source': None,
+                'annotator_1_id': None,
+                'annotator_2_id': None,
+                'duration_sec': None,
+                'title': None,
+                'artist': None,
+                'annotator_1_time': None,
+                'annotator_2_time': None,
+                'class': None,
+                'genre': None,
+            }
+
+        self.audio_path = utils.get_local_path(
+            self._data_home, self._track_paths['audio'][0])
+
+        self.source = self._track_metadata['source']
+        self.annotator_1_id = self._track_metadata['annotator_1_id']
+        self.annotator_2_id = self._track_metadata['annotator_2_id']
+        self.duration_sec = self._track_metadata['duration_sec']
+        self.title = self._track_metadata['title']
+        self.artist = self._track_metadata['artist']
+        self.annotator_1_time = self._track_metadata['annotator_1_time']
+        self.annotator_2_time = self._track_metadata['annotator_2_time']
+        self.broad_genre = self._track_metadata['class']
+        self.genre = self._track_metadata['genre']
+
+    @utils.cached_property
+    def sections_annotator_1_uppercase(self):
+        return _load_sections(utils.get_local_path(
+            self._data_home, self._track_paths['annotator_1_uppercase']))
+
+    @utils.cached_property
+    def sections_annotator_1_lowercase(self):
+        return _load_sections(utils.get_local_path(
+            self._data_home, self._track_paths['annotator_1_lowercase']))
+
+    @utils.cached_property
+    def sections_annotator_2_uppercase(self):
+        return _load_sections(utils.get_local_path(
+            self._data_home, self._track_paths['annotator_2_uppercase']))
+
+    @utils.cached_property
+    def sections_annotator_2_lowercase(self):
+        return _load_sections(utils.get_local_path(
+            self._data_home, self._track_paths['annotator_2_lowercase']))
 
 
 def download(data_home=None, force_overwrite=False):
     save_path = utils.get_save_path(data_home)
-    dataset_path = os.path.join(save_path, SALAMI_DIR)
+    dataset_path = os.path.join(save_path, DATASET_DIR)
 
     if exists(data_home) and not force_overwrite:
         return
 
     if force_overwrite:
-        utils.force_delete_all(SALAMI_ANNOT_REMOTE, dataset_path=None, data_home=data_home)
+        utils.force_delete_all(ANNOTATIONS_REMOTE, dataset_path=None, data_home=data_home)
 
     download_path = utils.download_from_remote(
-        SALAMI_ANNOT_REMOTE, data_home=data_home, force_overwrite=force_overwrite
+        ANNOTATIONS_REMOTE, data_home=data_home, force_overwrite=force_overwrite
     )
     if not os.path.exists(dataset_path):
         os.makedirs(dataset_path)
@@ -74,136 +115,67 @@ def download(data_home=None, force_overwrite=False):
 
 def exists(data_home=None):
     save_path = utils.get_save_path(data_home)
-    dataset_path = os.path.join(save_path, SALAMI_DIR)
+    dataset_path = os.path.join(save_path, DATASET_DIR)
     return os.path.exists(dataset_path)
 
 
 def validate(dataset_path, data_home=None):
     missing_files, invalid_checksums = utils.validator(
-        SALAMI_INDEX, data_home, dataset_path
+        INDEX, data_home, dataset_path
     )
     return missing_files, invalid_checksums
 
 
 def track_ids():
-    return list(SALAMI_INDEX.keys())
+    return list(INDEX.keys())
 
 
 def load(data_home=None):
     save_path = utils.get_save_path(data_home)
-    dataset_path = os.path.join(save_path, SALAMI_DIR)
+    dataset_path = os.path.join(save_path, DATASET_DIR)
 
     validate(dataset_path, data_home)
     salami_data = {}
     for key in track_ids():
-        salami_data[key] = load_track(key, data_home=data_home)
+        salami_data[key] = Track(key, data_home=data_home)
     return salami_data
 
 
-def load_track(track_id, data_home=None):
-    if track_id not in SALAMI_INDEX.keys():
-        raise ValueError('{} is not a valid track ID in Salami'.format(track_id))
-    track_data = SALAMI_INDEX[track_id]
-
-    if SALAMI_METADATA is None or SALAMI_METADATA['data_home'] != data_home:
-        _reload_metadata(data_home)
-        if SALAMI_METADATA is None:
-            raise EnvironmentError('Could not find Salami metadata file')
-
-    if track_id in SALAMI_METADATA.keys():
-        track_metadata = SALAMI_METADATA[track_id]
-    else:
-        # annotations with missing metadata
-        track_metadata = {
-            'source': None,
-            'annotator_1_id': None,
-            'annotator_2_id': None,
-            'duration_sec': None,
-            'title': None,
-            'artist': None,
-            'annotator_1_time': None,
-            'annotator_2_time': None,
-            'class': None,
-            'genre': None,
-        }
-    salami_path = utils.get_local_path(data_home, SALAMI_DIR)
-    annotations_dir = os.path.join(
-        salami_path, 'salami-data-public-master', 'annotations'
-    )
-    annotators = [
-        any(SALAMI_INDEX[track_id]['annotator_1_uppercase']),
-        any(SALAMI_INDEX[track_id]['annotator_2_uppercase']),
-    ]
-    all_annotators_section_data = _load_sections(
-        utils.get_local_path(annotations_dir, track_id), annotators
-    )
+def _load_sections(sections_path):
+    if sections_path is None:
+        return None
 
-    return SalamiTrack(
-        track_id,
-        utils.get_local_path(data_home, track_data['audio'][0]),
-        all_annotators_section_data[0],
-        all_annotators_section_data[1],
-        all_annotators_section_data[2],
-        all_annotators_section_data[3],
-        track_metadata['source'],
-        track_metadata['annotator_1_id'],
-        track_metadata['annotator_2_id'],
-        track_metadata['duration_sec'],
-        track_metadata['title'],
-        track_metadata['artist'],
-        track_metadata['annotator_1_time'],
-        track_metadata['annotator_2_time'],
-        track_metadata['class'],
-        track_metadata['genre'],
+    times = []
+    secs = []
+    with open(sections_path, 'r') as fhandle:
+        reader = csv.reader(fhandle, delimiter='\t')
+        for line in reader:
+            times.append(float(line[0]))
+            secs.append(line[1])
+    times = np.array(times)
+    secs = np.array(secs)
+
+    # remove sections with length == 0
+    times_revised = np.delete(times, np.where(np.diff(times) == 0))
+    secs_revised = np.delete(secs, np.where(np.diff(times) == 0))
+    return utils.SectionData(
+        np.array(times_revised[:-1]),
+        np.array(times_revised)[1:],
+        np.array(secs_revised)[:-1],
     )
 
 
-def _load_sections(sections_path, annotators):
-    all_annotators_section_data = []
-    for a in range(len(annotators)):
-        for f in ['uppercase.txt', 'lowercase.txt']:
-            times, secs = [], []
-            if annotators[a]:
-                file_path = os.path.join(
-                    sections_path, 'parsed', 'textfile{}_{}'.format(str(a + 1), f)
-                )
-                if os.path.exists(file_path):
-
-                    with open(file_path, 'r') as fhandle:
-                        reader = csv.reader(fhandle, delimiter='\t')
-                        for line in reader:
-                            times.append(float(line[0]))
-                            secs.append(line[1])
-                    times, secs = np.array(times), np.array(secs)
-                    # remove sections with length == 0
-                    times_revised = np.delete(times, np.where(np.diff(times) == 0))
-                    secs_revised = np.delete(secs, np.where(np.diff(times) == 0))
-                    all_annotators_section_data.append(
-                        utils.SectionData(
-                            np.array(times_revised[:-1]),
-                            np.array(times_revised)[1:],
-                            np.array(secs_revised)[:-1],
-                        )
-                    )
-                else:
-                    all_annotators_section_data.append(None)
-            else:
-                all_annotators_section_data.append(None)
-
-    return all_annotators_section_data
-
-
 def _load_metadata(data_home):
 
     metadata_path = utils.get_local_path(
         data_home,
         os.path.join(
-            SALAMI_DIR, 'salami-data-public-master', 'metadata', 'metadata.csv'
+            DATASET_DIR, 'salami-data-public-master', 'metadata', 'metadata.csv'
         ),
     )
 
     if not os.path.exists(metadata_path):
-        return None
+        raise OSError('Could not find Salami metadata file')
 
     with open(metadata_path, 'r') as fhandle:
         reader = csv.reader(fhandle, delimiter=',')
@@ -236,8 +208,8 @@ def _load_metadata(data_home):
 
 
 def _reload_metadata(data_home):
-    global SALAMI_METADATA
-    SALAMI_METADATA = _load_metadata(data_home=data_home)
+    global METADATA
+    METADATA = _load_metadata(data_home=data_home)
 
 
 def cite():
diff --git a/mirdata/utils.py b/mirdata/utils.py
index 7d95c55..e3fb1dc 100644
--- a/mirdata/utils.py
+++ b/mirdata/utils.py
@@ -103,11 +103,11 @@ def validator(dataset_index, data_home, dataset_path, silence=False):
 
 F0Data = namedtuple('F0Data', ['times', 'frequencies', 'confidence'])
 
-LyricsData = namedtuple(
-    'LyricsData', ['start_times', 'end_times', 'lyrics', 'pronounciations']
+LyricData = namedtuple(
+    'LyricData', ['start_times', 'end_times', 'lyrics', 'pronounciations']
 )
 
-SectionData = namedtuple('SectionsData', ['start_times', 'end_times', 'sections'])
+SectionData = namedtuple('SectionData', ['start_times', 'end_times', 'sections'])
 
 BeatData = namedtuple('BeatsData', ['beats_times', 'beats_positions'])
 
@@ -302,3 +302,22 @@ def force_delete_all(remote, dataset_path, data_home=None):
 
     if dataset_path:
         shutil.rmtree(dataset_path)
+
+
+class cached_property(object):
+    """ A property that is only computed once per instance and then replaces
+        itself with an ordinary attribute. Deleting the attribute resets the
+        property.
+        Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76
+    """
+
+    def __init__(self, func):
+        self.__doc__ = getattr(func, "__doc__")
+        self.func = func
+
+    def __get__(self, obj, cls):
+        # type: (Any, type) -> Any
+        if obj is None:
+            return self
+        value = obj.__dict__[self.func.__name__] = self.func(obj)
+        return value

Named tuple track objects to classes
Move all track namedtuples to classes to
(1) more easily document the meaning of each attribute
(2) lazy load larger annotation files
diff --git a/tests/test_beatles.py b/tests/test_beatles.py
index aeb89c7..35d868e 100644
--- a/tests/test_beatles.py
+++ b/tests/test_beatles.py
@@ -27,7 +27,7 @@ def save_path(data_home):
 
 @pytest.fixture
 def dataset_path(save_path):
-    return os.path.join(save_path, beatles.BEATLES_DIR)
+    return os.path.join(save_path, beatles.DATASET_DIR)
 
 
 @pytest.fixture
@@ -90,7 +90,7 @@ def test_download_force_overwrite(data_home,
 
     beatles.download(data_home, force_overwrite=True)
 
-    mock_force_delete_all.assert_called_once_with(beatles.BEATLES_ANNOT_REMOTE, dataset_path=None, data_home=data_home)
+    mock_force_delete_all.assert_called_once_with(beatles.ANNOTATIONS_REMOTE, dataset_path=None, data_home=data_home)
     mock_beatles_exists.assert_called_once()
     mock_download.assert_called_once()
     mock_untar.assert_called_once_with(mock_download.return_value, dataset_path, cleanup=True)
@@ -114,24 +114,12 @@ def test_validate_valid(dataset_path, mocker, mock_validator):
 
 
 def test_track_ids():
-    assert beatles.track_ids() == list(beatles.BEATLES_INDEX.keys())
+    assert beatles.track_ids() == list(beatles.INDEX.keys())
 
 
 def test_load_track_invalid_track_id():
     with pytest.raises(ValueError):
-        beatles.load_track('a-fake-track-id')
-
-
-def test_load_track_valid_track_id():
-    expected = beatles.BeatlesTrack(
-        '0401',
-        '/tmp/mir_datasets/Beatles/audio/04_-_Beatles_for_Sale/01_-_No_Reply.wav',
-        beats=None,
-        chords=None,
-        key=None,
-        sections=None,
-        title='01_-_No_Reply')
-    assert beatles.load_track('0401') == expected
+        beatles.Track('a-fake-track-id')
 
 
 def test_fix_newpoint():
diff --git a/tests/test_ikala.py b/tests/test_ikala.py
index 7453bfd..068c8b9 100644
--- a/tests/test_ikala.py
+++ b/tests/test_ikala.py
@@ -26,7 +26,7 @@ def save_path(data_home):
 
 @pytest.fixture
 def dataset_path(save_path):
-    return os.path.join(save_path, ikala.IKALA_DIR)
+    return os.path.join(save_path, ikala.DATASET_DIR)
 
 
 def test_validate_invalid(dataset_path, mocker, mock_validator):
diff --git a/tests/test_medleydb_melody.py b/tests/test_medleydb_melody.py
index 26dd2cb..b06b016 100644
--- a/tests/test_medleydb_melody.py
+++ b/tests/test_medleydb_melody.py
@@ -26,7 +26,7 @@ def save_path(data_home):
 
 @pytest.fixture
 def dataset_path(save_path):
-    return os.path.join(save_path, medleydb_melody.MEDLEYDB_MELODY_DIR)
+    return os.path.join(save_path, medleydb_melody.DATASET_DIR)
 
 
 def test_validate_invalid(dataset_path, mocker, mock_validator):
diff --git a/tests/test_medleydb_pitch.py b/tests/test_medleydb_pitch.py
index ee5d460..f2e05d0 100644
--- a/tests/test_medleydb_pitch.py
+++ b/tests/test_medleydb_pitch.py
@@ -1,7 +1,6 @@
 from __future__ import absolute_import
 
 import os
-import json
 
 import pytest
 
@@ -26,7 +25,7 @@ def save_path(data_home):
 
 @pytest.fixture
 def dataset_path(save_path):
-    return os.path.join(save_path, medleydb_pitch.MEDLEYDB_PITCH_DIR)
+    return os.path.join(save_path, medleydb_pitch.DATASET_DIR)
 
 
 def test_validate_valid(dataset_path, mocker, mock_validator):
diff --git a/tests/test_orchset.py b/tests/test_orchset.py
index cd6a8ba..744d6b6 100644
--- a/tests/test_orchset.py
+++ b/tests/test_orchset.py
@@ -1,7 +1,6 @@
 from __future__ import absolute_import
 
 import os
-import json
 
 import pytest
 
@@ -27,7 +26,7 @@ def save_path(data_home):
 
 @pytest.fixture
 def dataset_path(save_path):
-    return os.path.join(save_path, orchset.ORCHSET_DIR)
+    return os.path.join(save_path, orchset.DATASET_DIR)
 
 
 @pytest.fixture
@@ -85,7 +84,7 @@ def test_download_force_overwrite(data_home,
 
     orchset.download(data_home, force_overwrite=True)
 
-    mock_force_delete_all.assert_called_once_with(orchset.ORCHSET_META, dataset_path, data_home)
+    mock_force_delete_all.assert_called_once_with(orchset.REMOTE, dataset_path, data_home)
     mock_orchset_exists.assert_called_once()
     mock_download.assert_called_once()
     mock_unzip.assert_called_once_with(mock_download.return_value, save_path, dataset_path)
diff --git a/tests/test_salami.py b/tests/test_salami.py
index 44f2f82..ad49053 100644
--- a/tests/test_salami.py
+++ b/tests/test_salami.py
@@ -34,7 +34,7 @@ def mock_salami_exists(mocker):
 
 @pytest.fixture
 def dataset_path(save_path):
-    return os.path.join(save_path, salami.SALAMI_DIR)
+    return os.path.join(save_path, salami.DATASET_DIR)
 
 
 def test_download_already_exists(data_home, mocker,
@@ -93,7 +93,7 @@ def test_download_force_overwrite(data_home,
 
     salami.download(data_home, force_overwrite=True)
 
-    mock_force_delete_all.assert_called_once_with(salami.SALAMI_ANNOT_REMOTE, dataset_path=None, data_home=data_home)
+    mock_force_delete_all.assert_called_once_with(salami.ANNOTATIONS_REMOTE, dataset_path=None, data_home=data_home)
     mock_salami_exists.assert_called_once()
     mock_download.assert_called_once()
     mock_unzip.assert_called_once_with(mock_download.return_value, dataset_path, cleanup=True)
@@ -118,82 +118,82 @@ def test_validate_valid(dataset_path, mocker, mock_validator):
 
 def test_load_track_invalid_track_id():
     with pytest.raises(ValueError):
-        salami.load_track('a_fake_track')
+        salami.Track('a_fake_track')
 
 
 def test_load_track_no_metadata():
-    with pytest.raises(EnvironmentError):
-        salami.SALAMI_METADATA = None
-        salami.load_track('10')
+    with pytest.raises(OSError):
+        salami.METADATA = None
+        salami.Track('10')
 
 
 def test_load_track_wrong_metadata():
-    with pytest.raises(EnvironmentError):
-        salami.SALAMI_METADATA = {'data_home': 'test'}
-        salami.load_track('10', 'wrong_data_home')
-
-
-def test_load_track_missing_metadata(mock_load_sections):
-    data_home = 'fake-data-home'
-    salami.SALAMI_METADATA = {'data_home': data_home}
-    mock_load_sections.return_value = ['a', 'b', 'c', 'd']
-
-    expected_track = salami.SalamiTrack(
-        '10',
-        'fake-data-home/Salami/audio/10.mp3',
-        'a',
-        'b',
-        'c',
-        'd',
-        None,
-        None,
-        None,
-        None,
-        None,
-        None,
-        None,
-        None,
-        None,
-        None,
-    )
-
-    actual_track = salami.load_track('10', data_home)
-    assert actual_track == expected_track
-
-
-def test_load_track_with_metadata(mock_load_sections):
-    track_metadata = {
-        'source': 'source', 'annotator_1_id': 'aid-1', 'annotator_2_id': 'aid-2',
-        'duration_sec': 60, 'title': 'title', 'artist': 'artist',
-        'annotator_1_time': None, 'annotator_2_time': None, 'class': 'class',
-        'genre': 'genre'
-    }
-    data_home = 'fake-data-home'
-    salami.SALAMI_METADATA = {'data_home': data_home, '10': track_metadata}
-    mock_load_sections.return_value = ['a', 'b', 'c', 'd']
-
-    expected_track = salami.SalamiTrack(
-        '10',
-        'fake-data-home/Salami/audio/10.mp3',
-        'a',
-        'b',
-        'c',
-        'd',
-        'source',
-        'aid-1',
-        'aid-2',
-        60,
-        'title',
-        'artist',
-        None,
-        None,
-        'class',
-        'genre',
-    )
-
-    actual_track = salami.load_track('10', data_home)
-    assert actual_track == expected_track
+    with pytest.raises(OSError):
+        salami.METADATA = {'data_home': 'test'}
+        salami.Track('10', 'wrong_data_home')
+
+
+# def test_load_track_missing_metadata(mock_load_sections):
+#     data_home = 'fake-data-home'
+#     salami.METADATA = {'data_home': data_home}
+#     mock_load_sections.return_value = ['a', 'b', 'c', 'd']
+
+#     expected_track = salami.SalamiTrack(
+#         '10',
+#         'fake-data-home/Salami/audio/10.mp3',
+#         'a',
+#         'b',
+#         'c',
+#         'd',
+#         None,
+#         None,
+#         None,
+#         None,
+#         None,
+#         None,
+#         None,
+#         None,
+#         None,
+#         None,
+#     )
+
+#     actual_track = salami.load_track('10', data_home)
+#     assert actual_track == expected_track
+
+
+# def test_load_track_with_metadata(mock_load_sections):
+#     track_metadata = {
+#         'source': 'source', 'annotator_1_id': 'aid-1', 'annotator_2_id': 'aid-2',
+#         'duration_sec': 60, 'title': 'title', 'artist': 'artist',
+#         'annotator_1_time': None, 'annotator_2_time': None, 'class': 'class',
+#         'genre': 'genre'
+#     }
+#     data_home = 'fake-data-home'
+#     salami.METADATA = {'data_home': data_home, '10': track_metadata}
+#     mock_load_sections.return_value = ['a', 'b', 'c', 'd']
+
+#     expected_track = salami.SalamiTrack(
+#         '10',
+#         'fake-data-home/Salami/audio/10.mp3',
+#         'a',
+#         'b',
+#         'c',
+#         'd',
+#         'source',
+#         'aid-1',
+#         'aid-2',
+#         60,
+#         'title',
+#         'artist',
+#         None,
+#         None,
+#         'class',
+#         'genre',
+#     )
+
+#     actual_track = salami.load_track('10', data_home)
+#     assert actual_track == expected_track
 
 
 def test_track_ids():
-    assert salami.track_ids() == list(salami.SALAMI_INDEX.keys())
+    assert salami.track_ids() == list(salami.INDEX.keys())
diff --git a/tests/test_utils.py b/tests/test_utils.py
index e221a36..a00657c 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -152,13 +152,13 @@ def test_get_save_path_with_data_home():
 def test_download_from_remote(httpserver, tmpdir):
     httpserver.serve_content(open('tests/resources/remote.wav').read())
 
-    TEST_META = utils.RemoteFileMetadata(
+    TEST_REMOTE = utils.RemoteFileMetadata(
         filename='remote.wav',
         url=httpserver.url,
         checksum=('3f77d0d69dc41b3696f074ad6bf2852f')
     )
 
-    download_path = utils.download_from_remote(TEST_META, str(tmpdir))
+    download_path = utils.download_from_remote(TEST_REMOTE, str(tmpdir))
     expected_download_path = os.path.join(str(tmpdir), 'remote.wav')
     assert expected_download_path == download_path
 
@@ -166,14 +166,14 @@ def test_download_from_remote(httpserver, tmpdir):
 def test_download_from_remote_raises_IOError(httpserver, tmpdir):
     httpserver.serve_content('File not found!', 404)
 
-    TEST_META = utils.RemoteFileMetadata(
+    TEST_REMOTE = utils.RemoteFileMetadata(
         filename='remote.wav',
         url=httpserver.url,
         checksum=('1234')
     )
 
     with pytest.raises(IOError):
-        utils.download_from_remote(TEST_META, str(tmpdir))
+        utils.download_from_remote(TEST_REMOTE, str(tmpdir))
 
 
 def test_unzip(tmpdir):
@@ -229,18 +229,18 @@ def test_create_invalid(tmpdir,
 def test_force_delete_all_nonempty_data_home(httpserver, tmpdir):
     tmpdir_str = str(tmpdir)
     remote_filename = 'remote.wav'
-    TEST_META = utils.RemoteFileMetadata(
+    TEST_REMOTE = utils.RemoteFileMetadata(
         filename=remote_filename,
         url=httpserver.url,
         checksum=('1234')
     )
 
     with pytest.raises(IOError):
-        utils.download_from_remote(TEST_META, tmpdir_str)
+        utils.download_from_remote(TEST_REMOTE, tmpdir_str)
 
     utils.untar('tests/resources/remote.tar.gz', tmpdir_str)
     assert os.path.exists(os.path.join(tmpdir_str, remote_filename))
     assert os.path.exists(tmpdir_str)
-    utils.force_delete_all(TEST_META, tmpdir_str, tmpdir_str)
+    utils.force_delete_all(TEST_REMOTE, tmpdir_str, tmpdir_str)
     assert not os.path.exists(os.path.join(tmpdir_str, remote_filename))
     assert not os.path.exists(tmpdir_str)

pip install -e .[tests]
parse_log_pytest
pytest
apt-get update apt-get install -y gcc
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
audioread==3.0.1
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
coverage==7.8.0
coveralls==4.0.1
decorator==5.2.1
docker==7.1.0
docopt==0.6.2
exceptiongroup==1.2.2
execnet==2.1.1
future==0.17.1
idna==3.10
iniconfig==2.1.0
joblib==1.4.2
lazy_loader==0.4
librosa==0.11.0
llvmlite==0.43.0
MarkupSafe==3.0.2
-e git+https://github.com/mir-dataset-loaders/mirdata.git@d324391d7eac346b0599c26252daec7eeafa481b#egg=mirdata
msgpack==1.1.0
numba==0.60.0
numpy==2.0.2
packaging==24.2
pep8==1.7.1
platformdirs==4.3.7
pluggy==1.5.0
pooch==1.8.2
pycparser==2.22
pytest==8.3.5
pytest-cache==1.0
pytest-cov==6.0.0
pytest-localserver==0.9.0.post0
pytest-mock==3.14.0
pytest-pep8==1.0.6
python-dotenv==1.1.0
requests==2.32.3
scikit-learn==1.6.1
scipy==1.13.1
six==1.17.0
soundfile==0.13.1
soxr==0.5.0.post1
testcontainers==4.9.2
threadpoolctl==3.6.0
tomli==2.2.1
tqdm==4.67.1
typing_extensions==4.13.0
urllib3==2.3.0
Werkzeug==3.1.3
wrapt==1.17.2

name: mirdata
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - audioread==3.0.1
      - certifi==2025.1.31
      - cffi==1.17.1
      - charset-normalizer==3.4.1
      - coverage==7.8.0
      - coveralls==4.0.1
      - decorator==5.2.1
      - docker==7.1.0
      - docopt==0.6.2
      - exceptiongroup==1.2.2
      - execnet==2.1.1
      - future==0.17.1
      - idna==3.10
      - iniconfig==2.1.0
      - joblib==1.4.2
      - lazy-loader==0.4
      - librosa==0.11.0
      - llvmlite==0.43.0
      - markupsafe==3.0.2
      - msgpack==1.1.0
      - numba==0.60.0
      - numpy==2.0.2
      - packaging==24.2
      - pep8==1.7.1
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pooch==1.8.2
      - pycparser==2.22
      - pytest==8.3.5
      - pytest-cache==1.0
      - pytest-cov==6.0.0
      - pytest-localserver==0.9.0.post0
      - pytest-mock==3.14.0
      - pytest-pep8==1.0.6
      - python-dotenv==1.1.0
      - requests==2.32.3
      - scikit-learn==1.6.1
      - scipy==1.13.1
      - six==1.17.0
      - soundfile==0.13.1
      - soxr==0.5.0.post1
      - testcontainers==4.9.2
      - threadpoolctl==3.6.0
      - tomli==2.2.1
      - tqdm==4.67.1
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - werkzeug==3.1.3
      - wrapt==1.17.2
prefix: /opt/conda/envs/mirdata

tests/test_beatles.py::test_download_clean tests/test_beatles.py::test_download_force_overwrite tests/test_beatles.py::test_validate_invalid tests/test_beatles.py::test_validate_valid tests/test_beatles.py::test_track_ids tests/test_beatles.py::test_load_track_invalid_track_id tests/test_ikala.py::test_validate_invalid tests/test_ikala.py::test_validate_valid tests/test_medleydb_melody.py::test_validate_invalid tests/test_medleydb_melody.py::test_validate_valid tests/test_medleydb_pitch.py::test_validate_valid tests/test_orchset.py::test_download_clean tests/test_orchset.py::test_download_force_overwrite tests/test_orchset.py::test_validate_invalid tests/test_orchset.py::test_validate_valid tests/test_salami.py::test_download_clean tests/test_salami.py::test_download_force_overwrite tests/test_salami.py::test_validate_invalid tests/test_salami.py::test_validate_valid tests/test_salami.py::test_load_track_invalid_track_id tests/test_salami.py::test_load_track_no_metadata tests/test_salami.py::test_load_track_wrong_metadata tests/test_salami.py::test_track_ids
tests/test_beatles.py::test_download_already_exists tests/test_beatles.py::test_fix_newpoint tests/test_orchset.py::test_download_already_exists tests/test_salami.py::test_download_already_exists tests/test_utils.py::test_md5 tests/test_utils.py::test_check_index[test_index_valid.json-expected_missing0-expected_inv_checksum0] tests/test_utils.py::test_check_index[test_index_missing_file.json-expected_missing1-expected_inv_checksum1] tests/test_utils.py::test_check_index[test_index_invalid_checksum.json-expected_missing2-expected_inv_checksum2] tests/test_utils.py::test_validator[missing_files0-invalid_checksums0] tests/test_utils.py::test_validator[missing_files1-invalid_checksums1] tests/test_utils.py::test_validator[missing_files2-invalid_checksums2] tests/test_utils.py::test_validator_already_validated tests/test_utils.py::test_get_local_path[tests/-None-None] tests/test_utils.py::test_get_local_path[None-tests/-/root/mir_datasets/tests/] tests/test_utils.py::test_get_local_path[tests/-shoop-tests/shoop] tests/test_utils.py::test_get_save_path tests/test_utils.py::test_get_save_path_with_data_home tests/test_utils.py::test_download_from_remote tests/test_utils.py::test_download_from_remote_raises_IOError tests/test_utils.py::test_unzip tests/test_utils.py::test_untar tests/test_utils.py::test_check_validated tests/test_utils.py::test_create_validated tests/test_utils.py::test_create_invalid[missing_files0-invalid_checksums0] tests/test_utils.py::test_create_invalid[missing_files1-invalid_checksums1] tests/test_utils.py::test_force_delete_all_nonempty_data_home
mirdata/medleydb_melody.py mirdata/medleydb_pitch.py mirdata/orchset.py mirdata/beatles.py mirdata/salami.py mirdata/utils.py mirdata/maps.py mirdata/ikala.py
mirdata/medleydb_melody.py mirdata/medleydb_pitch.py mirdata/orchset.py mirdata/beatles.py mirdata/salami.py mirdata/utils.py mirdata/maps.py mirdata/ikala.py