xlance-msr / data /dataset.py
Jihuai's picture
no ban in other; configs
7a7b9a9
Raw
History Blame Contribute Delete
19.4 kB
from pathlib import Path
import random
import logging
import numpy as np
import librosa
import soundfile as sf
import json
from typing import List, Optional, Dict, Union, Tuple, Any
from torch.utils.data import Dataset, Sampler
from tqdm import tqdm
from data.augment import StemAugmentation, MixtureAugmentation
from data.moise_taxonomy import get_banned_other_pairs, get_target_stem_pairs
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
AUDIO_EXTENSIONS = ['.flac', '.mp3', '.wav']
DEFAULT_GAIN_RANGE = (0.5, 1.0)
def calculate_rms(audio: np.ndarray) -> float:
return np.sqrt(np.mean(audio**2))
def contains_audio_signal(audio: np.ndarray, rms_threshold: float = 0.01) -> bool:
if audio is None:
return False
return calculate_rms(audio) > rms_threshold
def fix_length(target: np.ndarray, source: np.ndarray) -> np.ndarray:
target_length, source_length = target.shape[-1], source.shape[-1]
if target_length < source_length:
return np.pad(target, ((0, 0), (0, source_length - target_length)), mode='constant')
if target_length > source_length:
return target[:, :source_length]
return target
def fix_length_to_duration(target: np.ndarray, duration: float, sr: int) -> np.ndarray:
target_length = target.shape[-1]
required_length = int(duration * sr)
if target_length < required_length:
return np.pad(target, ((0, 0), (0, required_length - target_length)), mode='constant')
if target_length > required_length:
return target[:, :required_length]
return target
def get_audio_duration(file_path: Path) -> float:
try:
return sf.info(file_path).duration
except Exception as e:
logger.error(f"Error getting duration for {file_path}: {e}")
return 0.0
def mix_to_target_snr(target: np.ndarray, noise: np.ndarray, target_snr_db: float) -> Tuple[np.ndarray, float, float]:
target_power, noise_power = np.mean(target ** 2), np.mean(noise ** 2)
if noise_power < 1e-8: return target.copy(), 1.0, 0.0
if target_power < 1e-8: return noise * 0.001, 0.0, 0.001
target_snr_linear = 10 ** (target_snr_db / 10)
noise_scale = np.sqrt(target_power / (noise_power * target_snr_linear))
scaled_noise = noise * noise_scale
mixture = target + scaled_noise
max_amplitude = np.max(np.abs(mixture))
target_scale = 1.0
if max_amplitude > 1.0:
norm_factor = 0.95 / max_amplitude
mixture *= norm_factor
target_scale = norm_factor
noise_scale *= norm_factor
return mixture, target_scale, noise_scale
class RawStems(Dataset):
def __init__(
self,
target_stem: str,
root_directory: Union[str, Path],
sr: int = 48000,
clip_duration: float = 3.0,
snr_range: Tuple[float, float] = (0.0, 10.0),
apply_augmentation: bool = True,
also_apply_mixture_augmentation: bool = True,
also_apply_stem_augmentation: bool = True,
rms_threshold: float = -40.0,
no_mixture: bool = False,
no_mixture_all: bool = False,
output_mixture: bool = False,
moisesdb: bool = False,
random_mixture: bool = False,
) -> None:
self.root_directory = Path(root_directory)
self.sr = sr
self.clip_duration = clip_duration
self.snr_range = snr_range
self.apply_augmentation = apply_augmentation
self.apply_stem_augmentation = apply_augmentation and also_apply_stem_augmentation
self.apply_mixture_augmentation = apply_augmentation and also_apply_mixture_augmentation
self.rms_threshold = rms_threshold
self.no_mixture = no_mixture
self.no_mixture_all = no_mixture_all
self.output_mixture = output_mixture
self.random_mixture = random_mixture
if moisesdb:
self.target_stems = get_target_stem_pairs(target_stem)
self.allowed_others = ["vocals", "bass", "drums", "guitar", "other_plucked", "percussion", "piano", "other_keys", "bowed_strings", "wind", "other"]
self.banned_others = []
else:
target_stem_parts = target_stem.split("_")
target_stem_1 = target_stem_parts[0].strip()
target_stem_2 = target_stem_parts[1].strip() if len(target_stem_parts) > 1 else None
self.target_stems = [(target_stem_1, target_stem_2)]
self.allowed_others = ["Kbs", "Gtr", "Bass", "Voc", "Synth", "Rhy", "Orch"]
assert target_stem_1 in self.allowed_others
self.banned_others = []
logger.info(f"Scanning '{self.root_directory}' for songs containing stem '{target_stem}'...")
self.folders = []
for song_dir in self.root_directory.iterdir():
if song_dir.is_dir():
for (target_stem_1, target_stem_2) in self.target_stems:
target_path = song_dir / target_stem_1
if target_stem_2:
target_path /= target_stem_2
if target_path.exists() and target_path.is_dir():
self.folders.append(song_dir)
break
if not self.folders:
raise FileNotFoundError(f"No subdirectories in '{self.root_directory}' were found containing the stem path '{target_stem}'. "
f"Please check your directory structure.")
logger.info(f"Found {len(self.folders)} song folders.")
self.audio_files = self._index_audio_files()
if not self.audio_files: raise ValueError("No audio files found.")
logger.info(f"Indexed {len(self.audio_files)} audio files.")
self.activity_masks = self._compute_activity_masks()
self._filter_activity_masks()
logger.info(f"{len(self.audio_files)} audio files after filtering.")
self.stem_augmentation = StemAugmentation()
if self.apply_mixture_augmentation:
self.mixture_augmentation = MixtureAugmentation()
def load_audio(self, file_path: Path, offset: float, duration: float, sr: int, aug: bool) -> np.ndarray:
expected_samples = int(sr * duration)
audio, _ = librosa.load(file_path, sr=sr, offset=offset, duration=duration, mono=False)
if len(audio.shape) == 1: audio = audio.reshape(1, -1)
if audio.shape[1] == 0: return np.zeros((2, expected_samples))
if audio.shape[0] == 1: audio = np.vstack([audio, audio])
if audio.shape[1] > expected_samples:
audio = audio[:, :expected_samples]
elif audio.shape[1] < expected_samples:
padding = expected_samples - audio.shape[1]
audio = np.pad(audio, ((0, 0), (0, padding)), mode='constant')
if aug and self.apply_stem_augmentation:
audio = self.stem_augmentation.apply(audio, self.sr)
return audio
def _compute_activity_masks(self) -> Dict[str, np.ndarray]:
rms_analysis_path = self.root_directory / "rms_analysis.jsonl"
if not rms_analysis_path.exists():
logger.warning("rms_analysis.jsonl not found. Non-silent selection will be disabled.")
return {}
logger.info(f"Loading and processing RMS data from {rms_analysis_path}")
rms_data = {}
with open(rms_analysis_path, 'r', encoding='utf-8') as f:
for line in f:
try:
data = json.loads(line)
rms_data[data['filepath']] = np.array(data['rms_db_per_second'])
except (json.JSONDecodeError, KeyError):
continue
logger.info("Computing activity masks for all indexed files...")
activity_masks = {}
window_size = int(np.ceil(self.clip_duration))
all_indexed_files = set()
for song in self.audio_files:
all_indexed_files.update(p.relative_to(self.root_directory) for p in song["target_stems"])
all_indexed_files.update(p.relative_to(self.root_directory) for p in song["others"])
for relative_path in tqdm(all_indexed_files, desc="Computing Activity Masks"):
path_str = str(relative_path)
if path_str in rms_data:
rms_values = rms_data[path_str]
if len(rms_values) < window_size:
activity_masks[path_str] = np.array([False] * len(rms_values))
continue
is_loud = rms_values > self.rms_threshold
sum_loud = np.convolve(is_loud, np.ones(window_size), 'valid')
avg_loud_enough = sum_loud / window_size > 0.8
mask = np.zeros(len(rms_values), dtype=bool)
mask[:len(avg_loud_enough)] = avg_loud_enough
activity_masks[path_str] = mask
else:
logger.warning(f"No RMS data found for {path_str}")
return activity_masks
def _filter_activity_masks(self) -> None:
def filter_stem(stem: Path) -> bool:
if not self._find_common_valid_start_seconds([stem]):
# logger.warning(f"Skipping {stem} due to silence.")
return False
return True
def filter_song(song: Dict[str, List[Path]]) -> bool:
if song["target_stems"] and song["others"]:
return True
# logger.warning(f"Skipping {song} due to empty or invalid audio.")
return False
for song in self.audio_files:
song["target_stems"] = list(filter(filter_stem, song["target_stems"]))
song["others"] = list(filter(filter_stem, song["others"]))
self.audio_files = list(filter(filter_song, self.audio_files))
def _find_common_valid_start_seconds(self, file_paths: List[Path]) -> List[int]:
if not self.activity_masks: return []
common_mask = None
min_len = float('inf')
masks_to_intersect = []
for file_path in file_paths:
path_str = str(file_path.relative_to(self.root_directory))
mask = self.activity_masks.get(path_str)
if mask is None: return []
masks_to_intersect.append(mask)
min_len = min(min_len, len(mask))
if not masks_to_intersect: return []
final_mask = np.ones(min_len, dtype=bool)
for mask in masks_to_intersect:
final_mask &= mask[:min_len]
return np.where(final_mask)[0].tolist()
def _index_audio_files(self) -> List[Dict[str, List[Path]]]:
indexed_songs = []
for folder in tqdm(self.folders, desc="Indexing audio files"):
song_dict = {"target_stems": [], "others": []}
for (target_stem_1, target_stem_2) in self.target_stems:
target_folder = folder / target_stem_1
if target_stem_2: target_folder /= target_stem_2
if target_folder.exists():
song_dict["target_stems"].extend(p for p in target_folder.rglob('*') if p.suffix.lower() in AUDIO_EXTENSIONS)
for p in folder.rglob('*'):
if p.suffix.lower() in AUDIO_EXTENSIONS:
try:
relative_path = p.relative_to(folder)
parts = relative_path.parts
if not (len(parts) > 0 and parts[0] in self.allowed_others):
# logger.warning(f"Skipping {p} due to unknown stem.")
raise ValueError
for (target_stem_1, target_stem_2) in self.target_stems + self.banned_others:
if len(parts) > 0 and parts[0] == target_stem_1 and (target_stem_2 is None or (len(parts) > 1 and parts[1] == target_stem_2)):
raise ValueError
song_dict["others"].append(p)
except ValueError:
continue
if song_dict["target_stems"] and song_dict["others"]:
indexed_songs.append(song_dict)
# else:
# logger.warning(f"Skipping {folder} due to empty or invalid audio.")
return indexed_songs
def load_other_audio_randomly(self, index: int, offset: float, duration: float, sr: int, aug: bool) -> np.ndarray:
song_dict = self.audio_files[index]
selected = random.choice(song_dict["others"])
valid_starts = self._find_common_valid_start_seconds([selected])
start_second = random.choice(valid_starts)
offset = start_second + random.uniform(0, 1.0 - (self.clip_duration % 1.0 or 1.0))
return self.load_audio(selected, offset, duration, sr, aug)
def __getitem__(self, index: int) -> Dict[str, Any]:
song_dict = self.audio_files[index]
for _ in range(100):
if self.no_mixture_all:
selected_target = random.choice(song_dict["target_stems"] + song_dict["others"])
valid_starts = self._find_common_valid_start_seconds([selected_target])
if not valid_starts:
continue
start_second = random.choice(valid_starts)
offset = start_second + random.uniform(0, 1.0 - (self.clip_duration % 1.0 or 1.0))
target = self.load_audio(selected_target, offset, self.clip_duration, self.sr, False)
if not contains_audio_signal(target):
continue
target_clean = target.copy()
target_augmented = self.stem_augmentation.apply(target, self.sr) if self.apply_stem_augmentation else target
return {
"mixture": np.nan_to_num(target_augmented),
"target": np.nan_to_num(target_clean)
}
if self.no_mixture:
num_targets = random.randint(1, min(len(song_dict["target_stems"]), 5))
selected_targets = random.sample(song_dict["target_stems"], num_targets)
valid_starts = self._find_common_valid_start_seconds(selected_targets)
if not valid_starts:
continue
start_second = random.choice(valid_starts)
offset = start_second + random.uniform(0, 1.0 - (self.clip_duration % 1.0 or 1.0))
target = sum(self.load_audio(p, offset, self.clip_duration, self.sr, False) for p in selected_targets) / num_targets
if not contains_audio_signal(target):
continue
target_clean = target.copy()
target_augmented = self.stem_augmentation.apply(target, self.sr) if self.apply_stem_augmentation else target
return {
"mixture": np.nan_to_num(target_augmented),
"target": np.nan_to_num(target_clean)
}
num_targets = random.randint(1, min(len(song_dict["target_stems"]), 5))
selected_targets = random.sample(song_dict["target_stems"], num_targets)
if not self.random_mixture:
num_others = random.randint(1, min(len(song_dict["others"]), 10))
selected_others = random.sample(song_dict["others"], num_others)
valid_starts = self._find_common_valid_start_seconds(selected_targets + selected_others)
else:
valid_starts = self._find_common_valid_start_seconds(selected_targets)
if valid_starts:
start_second = random.choice(valid_starts)
offset = start_second + random.uniform(0, 1.0 - (self.clip_duration % 1.0 or 1.0))
target_mix = sum(self.load_audio(p, offset, self.clip_duration, self.sr, False) for p in selected_targets) / num_targets # aug later
if not self.random_mixture:
other_mix = sum(self.load_audio(p, offset, self.clip_duration, self.sr, True) for p in selected_others) / num_others
else:
num_others = random.randint(1, 10)
selected_indices = random.sample(range(len(self.audio_files)), num_others)
other_mix = sum(self.load_other_audio_randomly(index, offset, self.clip_duration, self.sr, True) for index in selected_indices) / num_others
if not contains_audio_signal(target_mix) or not contains_audio_signal(other_mix):
# logger.warning(f"Skipping {song_dict} due to empty or invalid audio.")
continue
if not self.output_mixture:
target_clean = target_mix.copy()
target_augmented = self.stem_augmentation.apply(target_mix, self.sr) if self.apply_stem_augmentation else target_mix
mixture, target_scale, _ = mix_to_target_snr(
target_augmented, other_mix, random.uniform(*self.snr_range)
)
if not self.output_mixture:
target_clean *= target_scale
if self.output_mixture:
mixture_clean = mixture.copy()
mixture_augmented = self.mixture_augmentation.apply(mixture, self.sr) if self.apply_mixture_augmentation else mixture
max_val = np.max(np.abs(mixture_augmented)) + 1e-8
mixture_final = mixture_augmented / max_val
target_final = (mixture_clean if self.output_mixture else target_clean) / max_val
rescale = np.random.uniform(*DEFAULT_GAIN_RANGE)
mixture = np.nan_to_num(mixture_final * rescale)
target = np.nan_to_num(target_final * rescale)
target_length = int(self.clip_duration * self.sr)
if target.shape[1] != target_length:
target = np.pad(target, (0, target_length - target.shape[1]), mode='constant')
else:
target = target[:, :target_length]
if mixture.shape[1] != target_length:
mixture = np.pad(mixture, (0, target_length - mixture.shape[1]), mode='constant')
else:
mixture = mixture[:, :target_length]
return {
"mixture": np.nan_to_num(mixture).astype(np.float32),
"target": np.nan_to_num(target).astype(np.float32)
}
# logger.warning(f"No valid audio found for {song_dict}. Skipping.")
return self.__getitem__(random.randint(0, len(self.audio_files) - 1))
def __len__(self) -> int:
return len(self.audio_files)
class InfiniteSampler(Sampler):
def __init__(self, dataset: Dataset) -> None:
self.dataset_size = len(dataset)
self.indexes = list(range(self.dataset_size))
self.reset()
def reset(self) -> None:
random.shuffle(self.indexes)
self.pointer = 0
def __iter__(self):
while True:
if self.pointer >= self.dataset_size: self.reset()
yield self.indexes[self.pointer]
self.pointer += 1