"""Saraga-Carnatic-Melody-Synth loader
.. admonition:: Dataset Info
:class: dropdown
This dataset contains time aligned vocal melody and activations for Carnatic Music recordings, extracted
from the Saraga Carnatic dataset. The recordings have passed through a Carnatic-aware Analysis/Synthesis framework
to convert automatically extracted pitch tracks into ground-truth annotations. This dataset is not meant to be listened to,
but to be used as training and evaluation data for the vocal pitch extraction research of Indian Art Music.
The dataset contains a total of 2460 tracks, which generally have a length of 30 seconds, in some cases a bit less.
All the tracks have vocals at some point.
The files of this dataset are shared with the following license:
Creative Commons Attribution Non Commercial Share Alike 4.0 International
Dataset compiled by: GenĂs Plaja-Roglans, Thomas Nuttall, Lara Pearson, Xavier Serra, and Marius Miron.
For more information about Saraga Carnatic please refer to https://mtg.github.io/saraga/.
"""
import csv
import json
import os
from typing import BinaryIO, Optional, TextIO, Tuple
import librosa
import numpy as np
from smart_open import open
from mirdata import annotations, core, download_utils, io, jams_utils
BIBTEX = """
@article{Plaja-Roglans-2023,
author = {Plaja-Roglans, Gen{\'\i}s and Nuttall, Thomas and Pearson, Lara and Serra, Xavier and Miron, Marius},
doi = {10.5334/tismir.137},
journal = {Transactions of the International Society for Music Information Retrieval},
keyword = {en_US},
month = {Jun},
title = {Repertoire-Specific Vocal Pitch Data Generation for Improved Melodic Analysis of Carnatic Music},
year = {2023}
}
"""
INDEXES = {
"default": "1.0",
"test": "1.0",
"1.0": core.Index(filename="scms_index_1.0.json"),
}
REMOTES = {
"scms": download_utils.RemoteFileMetadata(
filename="Saraga-Carnatic-Melody-Synth.zip",
url="https://zenodo.org/record/5553925/files/Saraga-Carnatic-Melody-Synth.zip?download=1",
checksum="08322351d024f206e21abca962e495ab",
)
}
DOWNLOAD_INFO = None
LICENSE_INFO = (
"Creative Commons Attribution Non-Commercial Share-Alike 4.0 (CC BY-NC-SA 4.0)."
)
[docs]class Track(core.Track):
"""Saraga-Carnatic-Melody-Synth Track class
Args:
track_id (str): track id of the track
Attributes:
artist (str): artist
audio_path (str): path to the audio file
pitch_path (str): path to the pitch annotation file
activations_path (str): path to the vocal activation annotation file
tonic (str): tonic of the recording
gender (str): gender
artist (str): instrument of the track
title (str): title
train (bool): indicating if the track belongs to the train or testing set
track_id (str): track id
Cached Properties:
pitch (F0Data): vocal pitch time-series
activations (EventData): time regions where the singing voice is present and active
"""
def __init__(self, track_id, data_home, dataset_name, index, metadata):
super().__init__(track_id, data_home, dataset_name, index, metadata)
self.audio_path = self.get_path("audio")
self.pitch_path = self.get_path("pitch")
self.activations_path = self.get_path("activations")
@property
def tonic(self):
return self._track_metadata.get("tonic")
@property
def gender(self):
return self._track_metadata.get("gender")
@property
def artist(self):
return self._track_metadata.get("artist")
@property
def title(self):
return self._track_metadata.get("title")
@property
def train(self):
return self._track_metadata.get("train")
@core.cached_property
def pitch(self) -> Optional[annotations.F0Data]:
return load_pitch(self.pitch_path)
@core.cached_property
def activations(self) -> Optional[annotations.EventData]:
return load_activations(self.activations_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)
[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.pitch, "annotated pitch")],
event_data=[(self.activations, "vocal activations")],
metadata=self._track_metadata,
)
[docs]@io.coerce_to_bytes_io
def load_audio(fhandle: BinaryIO) -> Tuple[np.ndarray, float]:
"""Load a Saraga-Carnatic-Melody-Synth audio file.
Args:
fhandle (str or file-like): File-like object or path to audio file
Returns:
* np.ndarray - the mono audio signal
* float - The sample rate of the audio file
"""
return librosa.load(fhandle, sr=None, mono=True)
[docs]@io.coerce_to_string_io
def load_pitch(fhandle: TextIO) -> annotations.F0Data:
"""load a Saraga-Carnatic-Melody-Synth pitch annotation file
Args:
fhandle (str or file-like): str or file-like to pitch annotation file
Raises:
IOError: if the path doesn"t exist
Returns:
F0Data: pitch annotation
"""
times = []
freqs = []
voicing = []
reader = csv.reader(fhandle, delimiter=",")
for line in reader:
times.append(float(line[0]))
freq_val = float(line[1])
freqs.append(freq_val)
voicing.append(float(freq_val > 0))
return annotations.F0Data(
np.array(times), "s", np.array(freqs), "hz", np.array(voicing), "binary"
)
[docs]@io.coerce_to_string_io
def load_activations(fhandle: TextIO) -> Optional[annotations.EventData]:
"""load a Saraga-Carnatic-Melody-Synth activation annotation file
Args:
fhandle (str or file-like): str or file-like to note annotation file
Raises:
IOError: if file doesn"t exist
Returns:
EventData: vocal activations
"""
intervals = []
events = []
reader = csv.reader(fhandle, delimiter=",")
for line in reader:
intervals.append([float(line[0]), float(line[1])])
events.append(line[2].replace(" ", ""))
# if file is empty, return None
if len(intervals) == 0:
return None
return annotations.EventData(np.array(intervals), "s", events, "open")
[docs]@core.docstring_inherit(core.Dataset)
class Dataset(core.Dataset):
"""
The Saraga-Carnatic-Melody-Synth dataset
"""
def __init__(self, data_home=None, version="default"):
super().__init__(
data_home,
version,
name="scms",
track_class=Track,
bibtex=BIBTEX,
indexes=INDEXES,
remotes=REMOTES,
download_info=DOWNLOAD_INFO,
license_info=LICENSE_INFO,
)
@core.cached_property
def _artists_to_track_mapping(self):
mapping_path = os.path.join(
self.data_home, "SCMS/artists_to_track_mapping.json"
)
try:
with open(mapping_path, "r") as fhandle:
mapping = json.load(fhandle)
except FileNotFoundError:
raise FileNotFoundError(
"Artists to track mapping not found. Did you run .download()?"
)
return mapping
@core.cached_property
def _metadata(self):
metadata_path = os.path.join(self.data_home, "SCMS/metadata.json")
try:
with open(metadata_path, "r") as fhandle:
artists_metadata = json.load(fhandle)
except FileNotFoundError:
raise FileNotFoundError("Metadata not found. Did you run .download()?")
mapping = self._artists_to_track_mapping
idxs_and_artists = []
for artist in list(mapping.keys()):
for track in mapping[artist]:
idxs_and_artists.append((track, artist))
artist_info = {}
for subset in list(artists_metadata.keys()):
for artist, info in artists_metadata[subset].items():
artist_info[artist] = {
"tonic": info["tonic"],
"gender": info["gender"],
"train": True if subset == "train" else False,
}
metadata = {}
for idx in idxs_and_artists:
metadata[idx[0]] = {
"artist": idx[1],
"title": " ".join(idx[0].split("_")[:-1]),
"tonic": artist_info[idx[1]]["tonic"],
"gender": artist_info[idx[1]]["gender"],
"train": artist_info[idx[1]]["train"],
}
return metadata