Source code for mirdata.datasets.tonas

"""
TONAS Loader

.. admonition:: Dataset Info
    :class: dropdown

    This dataset contains a music collection of 72 sung excerpts representative of three a cappella singing styles
    (Deblas, and two variants of Martinete). It has been developed within the COFLA research project context.
    The distribution is as follows:
    1. 16 Deblas
    2. 36 Martinete 1
    3. 20 Martinete 2

    This collection was built in the context of a study on similarity and style classification of flamenco a cappella
    singing styles (Tonas) by the flamenco expert Dr. Joaquin Mora, Universidad de Sevilla.

    We refer to (Mora et al. 2010) for a comprehensive description of the considered styles and their musical
    characteristics. All 72 excerpts are monophonic, their average duration is 30 seconds and there is enough
    variability for a proper evaluation of our methods, including a variety of singers, recording conditions,
    presence of percussion, clapping, background voices and noise. We also provide manual melodic transcriptions,
    generated by the COFLA team and Cristina López Gómez.

    The annotations are represented by specifying the value (in this case, Notes and F0) at the related timestamps.
    TONAS' note and F0 annotations also have "Energy" information, which refers to the average energy value through
    all the frames in which a note or a F0 value is comprised.

    Using this dataset:
    TONAS dataset can be obtained upon request. Please refer to this link: https://zenodo.org/record/1290722 to
    request access and follow the indications of the .download() method for a proper storing and organization
    of the TONAS dataset.

    Citing this dataset:
    When TONAS is used for academic research, we would highly appreciate if scientific publications of works partly
    based on the TONAS dataset quote the following publication:
    - Music material: Mora, J., Gomez, F., Gomez, E., Escobar-Borrego, F.J., Diaz-Banez, J.M. (2010). Melodic
    Characterization and Similarity in A Cappella Flamenco Cantes. 11th International Society for Music Information
    Retrieval Conference (ISMIR 2010).
    - Transcriptions: Gomez, E., Bonada, J. (in Press). Towards Computer-Assisted Flamenco Transcription: An
    Experimental Comparison of Automatic Transcription Algorithms As Applied to A Cappella Singing.
    Computer Music Journal.


"""

import csv
import logging
import os
from typing import TextIO, Tuple, Optional

from deprecated.sphinx import deprecated
import librosa
import numpy as np
from smart_open import open

from mirdata import annotations, jams_utils, core, io


BIBTEX = """
Music material:
@inproceedings{tonas_music,
    author = {Mora, Joaquin and Gómez, Francisco and Gómez, Emilia
              and Borrego, Francisco Javier and Díaz-Báñez, José},
    year = {2010},
    month = {01},
    pages = {351-356},
    title = {Characterization and Similarity in A Cappella Flamenco Cantes.}
}

Transcriptions:
@inproceedings{tonas_annotations,
    author = {E. {Gómez} and J. {Bonada}},
    journal = {Computer Music Journal},
    title = {Towards Computer-Assisted Flamenco Transcription: An Experimental 
           Comparison of Automatic Transcription Algorithms as Applied to A 
           Cappella Singing},
    year = {2013},
    volume = {37},
    number = {2},
    pages = {73-90},
    doi = {10.1162/COMJ_a_00180}}
"""

INDEXES = {
    "default": "1.0",
    "test": "1.0",
    "1.0": core.Index(filename="tonas_index_1.0.json"),
}

DOWNLOAD_INFO = """
        PLEASE READ CAREFULLY ALL THE INFORMATION SO YOU DON'T MISS ANY STEP:
        Unfortunately, the TONAS dataset is not available to be shared openly. However,
        you can request access to the dataset in the following link, providing a brief
        explanation of what your are going to use the dataset for:
        ==> https://zenodo.org/record/1290722
        Then, unzip the dataset, change the dataset name to: "tonas" (with lowercase),
        and locate it to {}. If you unzip it into a different path, please remember to set the 
        right data_home when initializing the dataset.
"""

LICENSE_INFO = """
The TONAS dataset is offered free of charge for internal non-commercial use only. You can not redistribute it nor 
modify it. Dataset by COFLA team. Copyright © 2012 COFLA project, Universidad de Sevilla. Distribution rights granted 
to Music Technology Group, Universitat Pompeu Fabra. All Rights Reserved.
"""


[docs] class Track(core.Track): """TONAS track class Args: track_id (str): track id of the track data_home (str): Local path where the dataset is stored. If `None`, looks for the data in the default directory, `~/mir_datasets/TONAS` Attributes: f0_path (str): local path where f0 melody annotation file is stored notes_path(str): local path where notation annotation file is stored audio_path(str): local path where audio file is stored track_id (str): track id singer (str): performing singer (cantaor) title (str): title of the track song tuning_frequency (float): tuning frequency of the symbolic notation Cached Properties: f0_automatic (F0Data): automatically extracted f0 f0_corrected (F0Data): manually corrected f0 annotations notes (NoteData): annotated notes """ def __init__(self, track_id, data_home, dataset_name, index, metadata): super().__init__(track_id, data_home, dataset_name, index, metadata) self.f0_path = self.get_path("f0") self.notes_path = self.get_path("notes") self.audio_path = self.get_path("audio") @property def style(self): return self._track_metadata.get("style") @property def singer(self): return self._track_metadata.get("singer") @property def title(self): return self._track_metadata.get("title") @property def tuning_frequency(self): return _load_tuning_frequency(self.notes_path) @property def audio(self) -> Tuple[np.ndarray, float]: """The track's audio Returns: * np.ndarray - audio signal * float - sample rate """ return load_audio(self.audio_path) @core.cached_property def f0_automatic(self) -> Optional[annotations.F0Data]: return load_f0(self.f0_path, False) @core.cached_property def f0_corrected(self) -> Optional[annotations.F0Data]: return load_f0(self.f0_path, True) @property def f0(self): logging.warning( "Track.f0 is deprecated as of 0.3.4 and will be removed in a future version. Use" " Track.f0_automatic or Track.f0_corrected" ) return self.f0_corrected @core.cached_property def notes(self) -> Optional[annotations.NoteData]: return load_notes(self.notes_path)
[docs] 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, f0_data=[(self.f0, "pitch_contour")], note_data=[(self.notes, "note_hz")], metadata=self._track_metadata, )
[docs] def load_audio(fhandle: str) -> Tuple[np.ndarray, float]: """Load a TONAS audio file. Args: fhandle (str): path to an audio file Returns: * np.ndarray - the mono audio signal * float - The sample rate of the audio file """ return librosa.load(fhandle, sr=44100, mono=True)
# no decorator because of https://github.com/mir-dataset-loaders/mirdata/issues/503
[docs] def load_f0(fpath: str, corrected: bool) -> Optional[annotations.F0Data]: """Load TONAS f0 annotations Args: fpath (str): path pointing to f0 annotation file corrected (bool): if True, loads manually corrected frequency values otherwise, loads automatically extracted frequency values Returns: F0Data: predominant f0 melody """ times = [] freqs = [] freqs_corr = [] energies = [] with open(fpath, "r") as fhandle: reader = np.genfromtxt(fhandle) for line in reader: times.append(float(line[0])) freqs.append(float(line[2])) freqs_corr.append(float(line[3])) energies.append(float(line[1])) freq_array = np.array(freqs_corr if corrected else freqs, dtype="float") energy_array = np.array(energies, dtype="float") voicing_array = (freq_array > 0).astype("float") return annotations.F0Data( np.array(times, dtype="float"), "s", freq_array, "hz", voicing_array, "binary", energy_array, "energy", )
[docs] @io.coerce_to_string_io def load_notes(fhandle: TextIO) -> Optional[annotations.NoteData]: """Load TONAS note data from the annotation files Args: fhandle (str or file-like): path or file-like object pointing to a notes annotation file Returns: NoteData: note annotations """ intervals = [] pitches = [] energy = [] reader = csv.reader(fhandle, delimiter=",") tuning = next(reader)[0] for line in reader: intervals.append([line[0], float(line[0]) + float(line[1])]) # Convert midi value to frequency note_hz = _midi_to_hz(float(line[2]), float(tuning)) pitches.append(note_hz) energy.append(float(line[3])) note_data = annotations.NoteData( np.array(intervals, dtype="float"), "s", np.array(pitches, dtype="float"), "hz", np.array(energy, dtype="float"), "energy", ) return note_data
@io.coerce_to_string_io def _load_tuning_frequency(fhandle: TextIO) -> float: """Load tuning frequency of the track with re Args: fhandle (str or file-like): path or file-like object pointing to a notes annotation file Returns: tuning_frequency (float): returns new tuning frequency considering the deviation """ # Compute tuning frequency cents_deviation = float(next(csv.reader(fhandle, delimiter=","))[0]) tuning_frequency = 440 * ( 2 ** (cents_deviation / 1200) ) # Frequency of A (common value is 440Hz) return tuning_frequency def _midi_to_hz(midi_note, tuning_deviation): """Function to convert MIDI to Hz with certain tuning freq Args: midi_note (float): note represented in midi value tuning_deviation (float): deviation in cents with respect to 440Hz Returns: (float): note in Hz considering the new tuning frequency """ tuning_frequency = 440 * ( 2 ** (tuning_deviation / 1200) ) # Frequency of A (common value is 440Hz) return (tuning_frequency / 32) * (2 ** ((midi_note - 9) / 12))
[docs] @core.docstring_inherit(core.Dataset) class Dataset(core.Dataset): """ The TONAS dataset """ def __init__(self, data_home=None, version="default"): super().__init__( data_home, version, name="tonas", track_class=Track, bibtex=BIBTEX, indexes=INDEXES, download_info=DOWNLOAD_INFO, license_info=LICENSE_INFO, ) @core.cached_property def _metadata(self): metadata_path = os.path.join(self.data_home, "TONAS-Metadata.txt") metadata = {} try: with open(metadata_path, "r", errors="ignore") as f: reader = csv.reader( (x.replace("\0", "") for x in f), delimiter="\t" ) # Fix wrong byte for line in reader: if line: # Do not consider empty lines index = line[0].replace(".wav", "") metadata[index] = { "style": line[1], "title": line[2], "singer": line[3], } except FileNotFoundError: raise FileNotFoundError("Metadata not found. Did you run .download()?") return metadata
[docs] @deprecated(reason="Use mirdata.datasets.tonas.load_audio", version="0.3.4") def load_audio(self, *args, **kwargs): return load_audio(*args, **kwargs)
[docs] @deprecated(reason="Use mirdata.datasets.tonas.load_f0", version="0.3.4") def load_f0(self, *args, **kwargs): return load_f0(*args, **kwargs)
[docs] @deprecated(reason="Use mirdata.datasets.tonas.load_notes", version="0.3.4") def load_notes(self, *args, **kwargs): return load_notes(*args, **kwargs)