diff --git a/docs/source/mirdata.rst b/docs/source/mirdata.rst index 1df101318..5da4e0a73 100644 --- a/docs/source/mirdata.rst +++ b/docs/source/mirdata.rst @@ -59,6 +59,14 @@ billboard :inherited-members: +brid +^^^^ + +.. automodule:: mirdata.datasets.brid + :members: + :inherited-members: + + candombe ^^^^^^^^ diff --git a/docs/source/table.rst b/docs/source/table.rst index 79e312320..364b0da3f 100644 --- a/docs/source/table.rst +++ b/docs/source/table.rst @@ -63,6 +63,15 @@ - .. image:: https://licensebuttons.net/l/zero/1.0/80x15.png :target: http://creativecommons.org/publicdomain/zero/1.0/ + * - BRID + - - audio: ✅ + - annotations: ✅ + - - :ref:`beats` + - :ref:`tempo` + - 367 + - .. image:: https://licensebuttons.net/l/by-nc-sa/4.0/80x15.png + :target: https://creativecommons.org/licenses/by-nc-sa/4.0 + * - Candombe - - audio: ✅ - annotations: ✅ diff --git a/mirdata/datasets/brid.py b/mirdata/datasets/brid.py new file mode 100644 index 000000000..10e98a270 --- /dev/null +++ b/mirdata/datasets/brid.py @@ -0,0 +1,236 @@ +"""BRID Dataset Loader + +.. admonition:: Dataset Info + :class: dropdown + + The Brazilian Rhythmic Instruments Dataset (BRID) [1] is a valuable resource assembled for research in Music Information Retrieval (MIR). This dataset is designed to facilitate research in computational rhythm analysis, beat tracking, and rhythmic pattern recognition, particularly in the context of Brazilian music. BRID offers a comprehensive collection of solo and multiple-instrument recordings, featuring 10 different instrument classes playing in 5 main rhythm classes from Brazilian music, including samba, partido alto, samba-enredo, capoeira, and marcha. + + **Dataset Overview:** + + BRID comprises a total of 367 tracks, averaging about 30 seconds each, amounting to approximately 2 hours and 57 minutes of music. These tracks include recordings of various Brazilian instruments, played in different Brazilian rhythmic styles. + + **Instruments and Rhythms:** + + The recorded instruments in BRID represent the most significant instruments in Brazilian music, particularly samba. Ten different instrument classes were chosen, including agogoˆ, caixa (snare drum), cu ́ıca, pandeiro (frame drum), reco-reco, repique, shaker, surdo, tamborim, and tanta ̃. To ensure diversity in sound, these instruments vary in terms of shape, size, material, pitch/tuning, and the way they are struck, resulting in 32 variations. + + **Rhythms in BRID:** + + BRID features various Brazilian rhythmic styles, with a focus on samba and its sub-genres, samba-enredo and partido alto. Additionally, the dataset includes rhythms such as marcha, capoeira, and a few tracks of baia ̃o and maxixe styles. The dataset provides a faithful representation of each rhythm, all of which are in duple meter. + + **Dataset Recording:** + + All recordings in BRID were made in a professional recording studio in Manaus, Brazil, between October and November. + + **Applications:** + + The Brazilian Rhythmic Instruments Dataset (BRID) serves as a crucial resource for researchers in the field of Music Information Retrieval (MIR) and rhythm analysis. It showcases the richness of Brazilian rhythmic content and highlights the challenges that non-Western music presents to traditional computational musicology research. Researchers can use BRID to develop more robust MIR tools tailored to Brazilian music. + + **Acknowledgments:** + + We extend our gratitude to the creators of BRID for providing this valuable dataset for research purposes in the field of MIR. Additionally, we acknowledge the authors of the following research paper for their contributions to the dataset and experiments: + + [1] Lucas Maia, Pedro D. de Tomaz Júnior, Magdalena Fuentes, Martín Rocamora, Luiz W. P. Biscainho, Maurício V. M. Costa, and Sara Cohen. "A Novel Dataset of Brazilian Rhythmic Instruments and Some Experiments in Computational Rhythm Analysis." In Proceedings of the {CONGRESO LATINOAMERICANO DE LA AES}, 2018. [Link](https://api.semanticscholar.org/CorpusID:204762166) + + For more details on the dataset and its applications, please refer to the associated research papers and documentation. +""" + +import os +import csv +import logging +import librosa +import numpy as np +from typing import BinaryIO, Optional, TextIO, Tuple + +from mirdata import annotations, core, download_utils, io, jams_utils + + +BIBTEX = """ +@inproceedings{Maia2018AND, + title={A Novel Dataset of Brazilian Rhythmic Instruments and Some Experiments in Computational Rhythm Analysis}, + author={Lucas Maia and Pedro D. de Tomaz J{\'u}nior and Magdalena Fuentes and Mart{\'i}n Rocamora and Luiz W. P. Biscainho and Maur{\'i}cio V. M. Costa and Sara Cohen}, + year={2018}, + url={https://api.semanticscholar.org/CorpusID:204762166} +} +""" + +INDEXES = { + "default": "1.0", + "test": "sample", + "1.0": core.Index( + filename="brid_full_index_1.0.json", + url="https://zenodo.org/records/14052434/files/brid_full_index_1.0.json?download=1", + checksum="6292a6d36d6ae267534107f4e5f6bcca", + ), + "sample": core.Index(filename="brid_full_index_1.0_sample.json"), +} + + +REMOTES = { + "annotations": download_utils.RemoteFileMetadata( + filename="annotations.zip", + url="https://zenodo.org/records/14051323/files/annotations.zip?download=1", + checksum="678b2fa99c8d220cddd9f5e20d55d0c1", + destination_dir="BRID_1.0", + ), + "audio": download_utils.RemoteFileMetadata( + filename="audio.zip", + url="https://zenodo.org/records/14051323/files/audio.zip?download=1", + checksum="3514b53d66515181f95619adb71a59b4", + destination_dir="BRID_1.0", + ), +} + + +LICENSE_INFO = ( + "Creative Commons Attribution Non Commercial Share Alike 4.0 International." +) + + +class Track(core.Track): + """BRID Rhythm 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_path (str): path to audio file + beats_path (str): path to beats file + tempo_path (str): path to tempo file + + """ + + def __init__( + self, + track_id, + data_home, + dataset_name, + index, + metadata, + ): + super().__init__( + track_id, + data_home, + dataset_name, + index, + metadata, + ) + + # Audio path + self.audio_path = self.get_path("audio") + + # Annotations paths + self.beats_path = self.get_path("beats") + self.tempo_path = self.get_path("tempo") + + @core.cached_property + def beats(self) -> Optional[annotations.BeatData]: + return load_beats(self.beats_path) + + @core.cached_property + def tempo(self) -> Optional[float]: + return load_tempo(self.tempo_path) + + @property + def audio(self) -> Optional[Tuple[np.ndarray, float]]: + """The track's audio + + Returns: + * np.ndarray - audio signal + * float - sample rate + + """ + return load_audio(self.audio_path) + + def to_jams(self): + """Get the track's data in jams format + + Returns: + jams.JAMS: the track's data in jams format + + """ + return jams_utils.jams_converter( + audio_path=self.audio_path, + beat_data=[(self.beats, "beats")], + tempo_data=[(self.tempo, "tempo")], + metadata=None, + ) + + +def load_audio(audio_path): + """Load an audio file. + + Args: + audio_path (str): path to audio file + + Returns: + * np.ndarray - the mono audio signal + * float - The sample rate of the audio file + + """ + if audio_path is None: + return None + return librosa.load(audio_path, sr=44100, mono=False) + + +@io.coerce_to_string_io +def load_beats(fhandle: TextIO): + """Load beats + + Args: + fhandle (str or file-like): Local path where the beats annotation is stored. + + Returns: + BeatData: beat annotations + + """ + beat_times = [] + beat_positions = [] + + reader = csv.reader(fhandle, delimiter=" ") + for line in reader: + beat_times.append(float(line[0])) + beat_positions.append(int(line[1])) + + if not beat_times or beat_times[0] == -1.0: + return None + + return annotations.BeatData( + np.array(beat_times), "s", np.array(beat_positions), "bar_index" + ) + + +@io.coerce_to_string_io +def load_tempo(fhandle: TextIO) -> float: + """Load tempo + + Args: + fhandle (str or file-like): Local path where the tempo annotation is stored. + + Returns: + float: tempo annotation + + """ + reader = csv.reader(fhandle, delimiter=",") + return float(next(reader)[0]) + + +@core.docstring_inherit(core.Dataset) +class Dataset(core.Dataset): + """ + The BRID dataset + + """ + + def __init__(self, data_home=None, version="default"): + super().__init__( + data_home, + version, + name="brid", + track_class=Track, + bibtex=BIBTEX, + indexes=INDEXES, + remotes=REMOTES, + license_info=LICENSE_INFO, + ) diff --git a/scripts/make_brid_index.py b/scripts/make_brid_index.py new file mode 100644 index 000000000..7e44cb3be --- /dev/null +++ b/scripts/make_brid_index.py @@ -0,0 +1,71 @@ +import argparse +import hashlib +import json +import os +from mirdata.validate import md5 + +BRID_RHYTHM_INDEX_PATH = "../mirdata/datasets/indexes/brid_full_index_1.0.json" + +def make_brid_rhythm_index(dataset_data_path): + cmr_index = { + "version": "1.0", + "tracks": {}, + } + + dataset_folder_name = "BRID_1.0" # Adjust as needed for your dataset's root directory in mirdata + + # Define paths for beats and tempo annotations + beats_path = os.path.join(dataset_data_path, "Annotations", "beats") + tempo_path = os.path.join(dataset_data_path, "Annotations", "tempo") + + for root, dirs, files in os.walk(dataset_data_path): + for filename in files: + if filename.endswith(".wav"): + # Extract relevant path details + idx = filename.split(".")[0] + relative_audio_path = os.path.relpath(os.path.join(root, filename), dataset_data_path) + + # Construct paths for annotations + beat_file = f"{idx}.beats" + tempo_file = f"{idx}.bpm" + + # Set relative paths to None if files do not exist + relative_beat_path = os.path.join("Annotations", "beats", beat_file) if os.path.exists(os.path.join(beats_path, beat_file)) else None + relative_tempo_path = os.path.join("Annotations", "tempo", tempo_file) if os.path.exists(os.path.join(tempo_path, tempo_file)) else None + + + # Check if annotation files exist + beat_file_path = os.path.join(beats_path, beat_file) if relative_beat_path else None + tempo_file_path = os.path.join(tempo_path, tempo_file) if relative_tempo_path else None + + # Add track information to index + cmr_index["tracks"][idx] = { + "audio": ( + os.path.join(dataset_folder_name, relative_audio_path), + md5(os.path.join(root, filename)), + ), + "beats": ( + os.path.join(dataset_folder_name, relative_beat_path) if relative_beat_path else None, + md5(beat_file_path) if beat_file_path else None, + ), + "tempo": ( + os.path.join(dataset_folder_name, relative_tempo_path) if relative_tempo_path else None, + md5(tempo_file_path) if tempo_file_path else None, + ) + } + + # Write to index file + with open(BRID_RHYTHM_INDEX_PATH, "w") as fhandle: + json.dump(cmr_index, fhandle, indent=2) + +def main(args): + print("Creating index...") + make_brid_rhythm_index(args.dataset_data_path) + print("Index creation done!") + +if __name__ == "__main__": + PARSER = argparse.ArgumentParser(description="Make BRID Rhythm index file.") + PARSER.add_argument( + "dataset_data_path", type=str, help="Path to the BRID Rhythm data folder." + ) + main(PARSER.parse_args()) diff --git a/tests/datasets/test_brid.py b/tests/datasets/test_brid.py new file mode 100755 index 000000000..0c69662fd --- /dev/null +++ b/tests/datasets/test_brid.py @@ -0,0 +1,114 @@ +import os +import numpy as np +from mirdata import annotations +from mirdata.datasets import brid +from tests.test_utils import run_track_tests +import io + + +def test_track(): + default_trackid = "[0001] M4-01-SA" + data_home = os.path.normpath("tests/resources/mir_datasets/brid") + dataset = brid.Dataset(data_home, version="test") + track = dataset.track(default_trackid) + + expected_attributes = { + "track_id": "[0001] M4-01-SA", + "audio_path": os.path.join( + os.path.normpath("tests/resources/mir_datasets/brid/"), + "BRID_1.0/Data/Acoustic Mixtures/4 Instruments/[0001] M4-01-SA.wav", + ), + "beats_path": os.path.join( + os.path.normpath("tests/resources/mir_datasets/brid/"), + "BRID_1.0/Annotations/beats/[0001] M4-01-SA.beats", + ), + "tempo_path": os.path.join( + os.path.normpath("tests/resources/mir_datasets/brid/"), + "BRID_1.0/Annotations/tempo/[0001] M4-01-SA.bpm", + ), + } + + expected_property_types = { + "tempo": float, + "beats": annotations.BeatData, + "audio": tuple, + } + + run_track_tests(track, expected_attributes, expected_property_types) + + # test audio loading functions + _, sr = track.audio + assert sr == 44100 + + +def test_to_jams(): + data_home = "tests/resources/mir_datasets/brid" + dataset = brid.Dataset(data_home, version="test") + track = dataset.track("[0001] M4-01-SA") + jam = track.to_jams() + tempo = jam.search(namespace="tempo")[0]["data"] + assert [temp.value for temp in tempo] == [79.988654] + beats = jam.search(namespace="beat")[0]["data"] + assert len(beats) == 3 + assert [beat.time for beat in beats] == [0.025, 0.760, 1.490] + assert [beat.duration for beat in beats] == [0.0, 0.0, 0.0] + assert [beat.value for beat in beats] == [2, 1, 2] + assert [beat.confidence for beat in beats] == [None, None, None] + + +def test_load_tempo(): + data_home = "tests/resources/mir_datasets/brid" + dataset = brid.Dataset(data_home, version="test") + track = dataset.track("[0001] M4-01-SA") + tempo_path = track.tempo_path + parsed_tempo = brid.load_tempo(tempo_path) + assert parsed_tempo == 79.988654 + assert brid.load_tempo(None) is None + + +def test_load_beats(): + data_home = "tests/resources/mir_datasets/brid" + dataset = brid.Dataset(data_home, version="test") + track = dataset.track("[0001] M4-01-SA") + beats_path = track.beats_path + parsed_beats = brid.load_beats(beats_path) + + # Check types + assert type(parsed_beats) == annotations.BeatData + assert type(parsed_beats.times) is np.ndarray + assert type(parsed_beats.positions) is np.ndarray + + # Check values + assert np.array_equal(parsed_beats.times, np.array([0.025, 0.760, 1.490])) + assert np.array_equal(parsed_beats.positions, np.array([2, 1, 2])) + assert brid.load_beats(None) is None + + +def test_load_audio(): + data_home = "tests/resources/mir_datasets/brid" + dataset = brid.Dataset(data_home, version="test") + track = dataset.track("[0001] M4-01-SA") + audio_path = track.audio_path + audio, sr = brid.load_audio(audio_path) + + assert sr == 44100 + assert type(audio) == np.ndarray + + assert brid.load_audio(None) is None + + # Check for None case + assert ( + brid.load_beats(None) is None + ), "The function should return None when the input is None." + + # Case: beat_times[0] == -1.0 + invalid_beats_file = io.StringIO("-1.0\t2\n") + assert ( + brid.load_beats(invalid_beats_file) is None + ), "The function should return None when the first beat time is -1.0." + + # Case: empty beat_times + empty_beats_file = io.StringIO("") + assert ( + brid.load_beats(empty_beats_file) is None + ), "The function should return None when the beat times are empty." diff --git a/tests/indexes/brid_full_index_1.0_sample.json b/tests/indexes/brid_full_index_1.0_sample.json new file mode 100644 index 000000000..89f3349c1 --- /dev/null +++ b/tests/indexes/brid_full_index_1.0_sample.json @@ -0,0 +1,19 @@ +{ + "version": "1.0", + "tracks": { + "[0001] M4-01-SA": { + "audio": [ + "BRID_1.0/Data/Acoustic Mixtures/4 Instruments/[0001] M4-01-SA.wav", + "7b5605eabd42493158c3a250ddbb1810" + ], + "beats": [ + "BRID_1.0/Annotations/beats/[0001] M4-01-SA.beats", + "d37a01c33a34597a0e60fb5e3d8a89a6" + ], + "tempo": [ + "BRID_1.0/Annotations/tempo/[0001] M4-01-SA.bpm", + "4aa10c8164de322246e624fd7c920402" + ] + } + } +} \ No newline at end of file diff --git a/tests/resources/mir_datasets/brid/BRID_1.0/Annotations/beats/[0001] M4-01-SA.beats b/tests/resources/mir_datasets/brid/BRID_1.0/Annotations/beats/[0001] M4-01-SA.beats new file mode 100644 index 000000000..43bca9d1b --- /dev/null +++ b/tests/resources/mir_datasets/brid/BRID_1.0/Annotations/beats/[0001] M4-01-SA.beats @@ -0,0 +1,3 @@ +0.025 2 +0.760 1 +1.490 2 diff --git a/tests/resources/mir_datasets/brid/BRID_1.0/Annotations/tempo/[0001] M4-01-SA.bpm b/tests/resources/mir_datasets/brid/BRID_1.0/Annotations/tempo/[0001] M4-01-SA.bpm new file mode 100644 index 000000000..5d2a018ac --- /dev/null +++ b/tests/resources/mir_datasets/brid/BRID_1.0/Annotations/tempo/[0001] M4-01-SA.bpm @@ -0,0 +1 @@ +79.988654 diff --git a/tests/resources/mir_datasets/brid/BRID_1.0/Data/Acoustic Mixtures/4 Instruments/[0001] M4-01-SA.wav b/tests/resources/mir_datasets/brid/BRID_1.0/Data/Acoustic Mixtures/4 Instruments/[0001] M4-01-SA.wav new file mode 100644 index 000000000..1c1d65207 Binary files /dev/null and b/tests/resources/mir_datasets/brid/BRID_1.0/Data/Acoustic Mixtures/4 Instruments/[0001] M4-01-SA.wav differ diff --git a/tests/test_loaders.py b/tests/test_loaders.py index bb9a53b48..3f7d654d0 100644 --- a/tests/test_loaders.py +++ b/tests/test_loaders.py @@ -13,6 +13,7 @@ DATASETS = mirdata.DATASETS CUSTOM_TEST_TRACKS = { "beatles": "0111", + "brid": "[0001] M4-01-SA", "ballroom": "Media-105901", "cante100": "008", "compmusic_carnatic_rhythm": "10003",