import os import json import glob import torch import torchaudio import numpy as np from tqdm import tqdm from typing import List from suno_utils.utils.s3 import read_from_s3 from suno_utils.models.dac.nn.quantize_2 import ResidualVectorQuantize # ---------------- functional corruptions ---------------- def apply_stereo_to_mono(audio: torch.tensor, sample_rate: float): return audio.mean(dim=0, keepdims=True).repeat(2, 1) def apply_overlay_copy(audio: torch.tensor, sample_rate: float): copy_size = np.random.randint(audio.shape[-1] // 4, audio.shape[-1]) start_idx = np.random.randint(0, audio.shape[-1] - copy_size) audio_copy = audio[:, start_idx : start_idx + copy_size] copy_start_idx = np.random.randint(0, audio.shape[-1] - copy_size) gain_db = np.random.rand() * -8 gain_lin = 10 ** (gain_db / 20.0) audio_out = audio.clone() audio_out[:, copy_start_idx : copy_start_idx + copy_size] += gain_lin * audio_copy return audio_out def apply_drop_random_samples(audio: torch.tensor, sample_rate: float): p = torch.rand(1).item() mask = torch.bernoulli(torch.full(audio.size(), p)) return audio * mask def apply_random_tanh_distortion(audio: torch.tensor, sample_rate: float): gain_db = np.random.rand() * 18 gain_lin = 10 ** (gain_db / 20.0) return torch.tanh(audio * gain_lin) def apply_random_clipping_distortion(audio: torch.tensor, sample_rate: float): gain_db = np.random.rand() * 18 gain_lin = 10 ** (gain_db / 20.0) return (audio * gain_lin).clamp(-1, 1) def apply_random_white_noise(audio: torch.Tensor, sample_rate: float): gain_db = (np.random.rand() * -48) - 18 gain_lin = 10 ** (gain_db / 20.0) noise = gain_lin * torch.randn(audio.shape) return noise + audio # ---------------- ffmpeg filter corruptions ------------- def get_random_bitcrusher_filter(): bit_depths = [1, 2, 3, 4, 5, 6] aas = [0, 1] modes = ["lin", "log"] mix = np.random.rand() / 2 bits = np.random.choice(bit_depths) mode = np.random.choice(modes) aa = np.random.choice(aas) return ( f"acrusher=level_in=1:level_out=0.8:bits={bits}:mode={mode}:mix={mix:0.3f}:aa=1" ) def get_random_exciter_filter(): amounts = np.arange(1, 64) drives = np.linspace(0.1, 10.0, num=50) blends = np.linspace(-10.0, 10.0, num=50) freqs = np.linspace(2000.0, 12000.0, num=50) ceils = np.linspace(9999.9, 20000.0, num=50) amount = np.random.choice(amounts) drive = np.random.choice(drives) blend = np.random.choice(blends) freq = np.random.choice(freqs) ceil = np.random.choice(ceils) return f"aexciter=level_in=1:level_out=0.8:amount={amount}:drive={drive}:blend={blend}:freq={freq}:ceil={ceil}" def get_random_psyclip_filter(): clips = np.linspace(-24.0, -4.0, num=24) clips = np.linspace(0.5, 1.0, num=25) diffs = [0, 1] adaptives = np.linspace(0.0, 1.0, num=10) clip = np.random.choice(clips) diff = np.random.choice(diffs) adaptive = np.random.choice(adaptives) return ( f"apsyclip=level_in=1:level_out=0.8:clip={clip}:diff={diff}:adaptive={adaptive}" ) def get_random_pulsator_filter(): modes = ["sine", "triangle", "square", "sawup", "sawdown"] amounts = np.linspace(0.0, 1.0, num=20) bpms = np.linspace(30, 300, num=100) widths = np.linspace(0.0, 2.0, num=20) mode = np.random.choice(modes) amount = np.random.choice(amounts) width = np.random.choice(widths) bpm = np.random.choice(bpms) return f"apulsator=level_in=1:level_out=1.0:mode={mode}:amount={amount}:width={width}:timing=bpm:bpm={bpm}" def get_random_denoiser_filter(): pass def get_random_softclip_filter(): clip_types = [ "hard", "tanh", "atan", "cubic", "exp", "alg", "quintic", "sin", "erf", ] thresholds = np.linspace(0.125, 1.0, num=25) clip_type = np.random.choice(clip_types) threshold = np.random.choice(thresholds) return f"asoftclip=type={clip_type}:threshold={threshold}" def get_random_bandpass_filter(): frequencies = np.linspace(100.0, 10000.0, num=1000) width_type = "q" widths = [0.707, 1.0, 2.0, 4.0, 8.0, 10.0] frequency = np.random.choice(frequencies) width = np.random.choice(widths) return f"bandpass=frequency={frequency}:width_type={width_type}:width={width}" def get_random_bandreject_filter(): frequencies = np.linspace(100.0, 10000.0, num=1000) width_type = "q" widths = [0.707, 1.0, 2.0, 4.0, 8.0, 10.0] frequency = np.random.choice(frequencies) width = np.random.choice(widths) return f"bandreject=frequency={frequency}:width_type={width_type}:width={width}" def get_random_crystalizer_filter(): i = -(np.random.rand() * 10) return f"crystalizer=i={i}" def get_random_dcshift_filter(): shift = (np.random.rand() * 2) - 1 limitergain = np.random.rand() * 0.1 return f"dcshift=shift={shift}:limitergain={limitergain}" def get_random_deesser_filter(): i = np.random.rand() m = np.random.rand() f = np.random.rand() return f"deesser=i={i}:m={m}:f={f}" def get_random_highpass_filter(): frequencies = np.linspace(100.0, 10000.0, num=100) poless = [1, 2] width_type = "q" widths = [0.707, 1.0, 2.0, 4.0, 8.0, 10.0] frequency = np.random.choice(frequencies) poles = np.random.choice(poless) width = np.random.choice(widths) return f"highpass=frequency={frequency}:poles={poles}:width_type={width_type}:width={width}" def get_random_lowpass_filter(): frequencies = np.logspace(1, np.log10(4000), num=100) poless = [1, 2] width_type = "q" widths = [0.707, 1.0, 2.0, 4.0, 8.0, 10.0] frequency = np.random.choice(frequencies) poles = np.random.choice(poless) width = np.random.choice(widths) return f"lowpass=frequency={frequency}:poles={poles}:width_type={width_type}:width={width}" def get_4k_lowpass_filter(): poless = [1, 2] width_type = "q" widths = [0.707, 1.0, 2.0, 4.0, 8.0, 10.0] frequency = 4000.0 poles = np.random.choice(poless) width = np.random.choice(widths) return f"lowpass=frequency={frequency}:poles={poles}:width_type={width_type}:width={width}" def get_6k_lowpass_filter(): poless = [1, 2] width_type = "q" widths = [0.707, 1.0, 2.0, 4.0, 8.0, 10.0] frequency = 6000.0 poles = np.random.choice(poless) width = np.random.choice(widths) return f"lowpass=frequency={frequency}:poles={poles}:width_type={width_type}:width={width}" def apply_audio_filters( audio: torch.tensor, sample_rate: float, filter_string: str, apply_mp3_codec: bool = False, ): """Apply FFmpeg audio filters to audio tensor via torchaudio `AudioEffector` interface. Args: audio (torch.Tensor): Stereo audio tensor (2, samples). sample_rate (float): Audio sample rate. filter_string (str): String defining a series of FFmpeg audio filters. apply_mp3_codec (bool): Apply mp3 codec to output audio. Returns: torch.Tensor: Processed stereo audio tensor (2, samples). """ if apply_mp3_codec: bit_rates = [ 8_000, 16_000, 24_000, 32_000, 40_000, 48_000, 64_000, 80_000, 96_000, 112_000, 128_000, ] bit_rate = np.random.choice(bit_rates) effector = torchaudio.io.AudioEffector( filter_string, format="mp3", codec_config=torchaudio.io.CodecConfig(bit_rate=bit_rate), ) else: effector = torchaudio.io.AudioEffector(filter_string) return effector.apply(audio.T, sample_rate).T def corrupt_audio( audio: torch.Tensor, sample_rate: int, corrupts: List[str], corrupt_probs: List[float], ffmpeg_filters: List[str], ffmpeg_filter_probs: List[float], mp3_codec_prob: float, ): assert len(corrupts) == len(corrupt_probs) assert len(ffmpeg_filters) == len(ffmpeg_filter_probs) # apply corruptions in sequence to audio if len(corrupts) > 0: # create a random ordering of the funcs func_order = torch.randperm(len(corrupts)) for func_idx in func_order: # sample whether each func is active or not based on prob if torch.rand(1).item() < corrupt_probs[func_idx]: corrupt_func = CORRUPTS[corrupts[func_idx]] audio = corrupt_func(audio, sample_rate) filter_string = "" if len(ffmpeg_filters) > 0: # create a random ordering of the filters func_order = torch.randperm(len(ffmpeg_filters)) for idx, func_idx in enumerate(func_order): # sample whether each func is active or not based on prob if torch.rand(1).item() < ffmpeg_filter_probs[func_idx]: ffmpeg_filter_func = FFMPEG_FILTERS[ffmpeg_filters[func_idx]] filter_string += ffmpeg_filter_func() if (idx + 1) != len(ffmpeg_filters): filter_string += "," filter_string = filter_string.strip(",") # remove trailing comma if filter_string == "": # if no filters, set to None filter_string = None # apply the composite filters (if any) and mp3 codec audio = apply_audio_filters( audio, sample_rate, filter_string, apply_mp3_codec=True if np.random.rand() < mp3_codec_prob else False, ) if audio.abs().max() > 1.0: audio /= audio.abs().max() return audio CORRUPTS = { "stereo_to_mono": apply_stereo_to_mono, "drop_random_samples": apply_drop_random_samples, "tanh_distortion": apply_random_tanh_distortion, "clipping_distortion": apply_random_clipping_distortion, "white_noise": apply_random_white_noise, "overlay_copy": apply_overlay_copy, } # mapping from strings to func FFMPEG_FILTERS = { "bitcrusher": get_random_bitcrusher_filter, "exciter": get_random_exciter_filter, "pulsator": get_random_pulsator_filter, "bandpass": get_random_bandpass_filter, "bandreject": get_random_bandreject_filter, "crystalizer": get_random_crystalizer_filter, "dcshift": get_random_dcshift_filter, "highpass": get_random_highpass_filter, "deesser": get_random_deesser_filter, "lowpass": get_random_lowpass_filter, "softclip": get_random_softclip_filter, "4k_lowpass": get_4k_lowpass_filter, } class AudioCorruptionDataset(torch.utils.data.Dataset): def __init__(self, filepaths: List[str], num_frames: int, chunks_per_file: int): self.filepaths = filepaths self.num_frames = num_frames self.chunks_per_file = chunks_per_file self.examples = [] # create examples by chunking into num_frames chunks print(f"Creating file chunks of {num_frames} frames.") for filepath in tqdm(filepaths): # file_num_frames = torchaudio.info(filepath).num_frames # num_chunks = file_num_frames // num_frames # frame_offsets = [ # (chunk_idx * num_frames) for chunk_idx in range(num_chunks) # ] # for frame_offset in frame_offsets: # self.examples.append((filepath, frame_offset)) for _ in range(chunks_per_file): self.examples.append(filepath) print(f"Total of {len(self.examples)} chunks.") def __len__(self): return len(self.examples) def __getitem__(self, idx): filepath = self.examples[idx] valid = True file_num_frames = torchaudio.info(filepath).num_frames if file_num_frames < self.num_frames: valid = False audio = torch.zeros(2, self.num_frames) corrupted_audio = torch.zeros(2, self.num_frames) else: frame_offset = np.random.randint(0, file_num_frames - self.num_frames - 1) # read random 10 sec chunk from disk audio, sample_rate = torchaudio.load( filepath, frame_offset=frame_offset, num_frames=self.num_frames ) # run through randomized corruption pipeline corrupted_audio = corrupt_audio( audio, sample_rate, corrupts, corrupt_probs, ffmpeg_filters, ffmpeg_filter_probs, mp3_codec_prob, ) return audio, corrupted_audio, valid def load_rvq( codec_path: str, input_dim: int, n_codebooks: int, codebook_size: int, codebook_dim: int, quantizer_dropout: int, ): sd = read_from_s3(codec_path, read_f=torch.load) model = ResidualVectorQuantize( input_dim=input_dim, n_codebooks=n_codebooks, codebook_size=codebook_size, codebook_dim=codebook_dim, quantizer_dropout=quantizer_dropout, ) model.load_state_dict( {k[10:]: v for k, v in sd["state_dict"].items() if k.startswith("quantize")} ) model.eval() for param_name, param in model.named_parameters(): param.requires_grad = False return model # convert codes to latent embedding def decode_vq(rvq_model, codes, n_quantizers): z_q = 0 for i, quantizer in enumerate(rvq_model.quantizers[:n_quantizers]): _z_q = quantizer.embed_code(codes[:, :, i]).transpose(1, 2) _z_q = quantizer.out_proj(_z_q) z_q += _z_q.transpose(1, 2) return z_q if __name__ == "__main__": # given a directory of audio files # load pretrained embedding model # read all audio files from disk # apply random corruptions to the audio # embed both input and corrupted version with model # store each seq of embedding to separate memmap with shared index use_val = False batch_size = 32 chunks_per_file = 1 num_frames = 480000 model_type = "dac_2c_25x12" if model_type == "dac_100hz_12cb": n_codebooks = 12 n_codes = 1000 embed_dim = 128 example_size = int(n_codebooks * n_codes) from suno_utils.tasks.dac_peaq100 import ( preload_models as preload_vae_models, encode as vae_encode, decode as vae_decode, ) # load VAE model = preload_vae_models( checkpoint_filepath="s3://suno-data/minz/models/dac_100hz_12cb.pth" ) elif model_type == "mw_vae_peaq_128_fix": n_codebooks = 12 n_codes = 250 embed_dim = 128 example_size = int(embed_dim * n_codes) # VAE from suno_utils.tasks.dac_vae_peaq import ( preload_models as preload_vae_models, encode as vae_encode, decode as vae_decode, ) # load VAE model = preload_vae_models( checkpoint_filepath="s3://suno-data/christian/mw_vae_peaq_128_fix.pth" ) elif model_type == "dac_2c_25x12": n_codebooks = 12 n_codes = 250 embed_dim = 128 example_size = int(n_codebooks * n_codes) # VAE from suno_utils.tasks.dac_2c_12cb import ( preload_models as preload_vae_models, encode as vae_encode, decode as vae_decode, ) # load VAE model = preload_vae_models( checkpoint_filepath="s3://suno-data/georg/models/codec/dac_2c_25x12.pt" ) else: raise ValueError(f"Invalid model_type: {model_type}") root_dir = "/app/suno/data/audio_2ch_48khz_lg/" root_dir = root_dir + "val" if use_val else root_dir + "train" mm_out_dir = f"/app/suno/christian/data/enhance_{model_type}/" os.makedirs(mm_out_dir, exist_ok=True) if use_val: input_mm_filepath = os.path.join(mm_out_dir, "input_val.bin") corrupt_mm_filepath = os.path.join(mm_out_dir, "corrupt_val.bin") else: input_mm_filepath = os.path.join(mm_out_dir, "input_tr.bin") corrupt_mm_filepath = os.path.join(mm_out_dir, "corrupt_tr.bin") subsets = [ "genius_hq", "imslp", "jamendo", # "podcasts", # "pond5_music", # "pond5_sfx", # "shutter_music", "spot_genres", # "tency", # "youtube_music", ] # setup corruption pipeline configuration corrupts = [ "stereo_to_mono", "drop_random_samples", "tanh_distortion", "clipping_distortion", "white_noise", "overlay_copy", ] corrupt_probs = [0.25, 0.2, 0.3, 0.3, 0.3, 0.1] ffmpeg_filters = [ "bitcrusher", "exciter", "bandpass", "highpass", "deesser", "lowpass", ] ffmpeg_filter_probs = [0.1, 0.1, 0.2, 0.2, 0.2, 0.2] mp3_codec_prob = 0.9 # load audio subset_dirs = glob.glob(os.path.join(root_dir, "*")) # search for audio filepaths = [] for subset_dir in subset_dirs: subset_name = os.path.basename(subset_dir) if subset_name not in subsets: continue print(subset_dir) subset_filepaths = glob.glob(os.path.join(subset_dir, "*.wav")) print(f"Found {len(subset_filepaths)} in {subset_name} (use_val={use_val}).") filepaths.extend(subset_filepaths) # create dataloader for multiprocessing dataset = AudioCorruptionDataset(filepaths, num_frames, chunks_per_file) dataloader = torch.utils.data.DataLoader( dataset, batch_size=batch_size, num_workers=batch_size, shuffle=False, ) # create memmap filepointers num_examples = len(dataset) input_mm = np.memmap( input_mm_filepath, dtype=np.uint16, mode="w+", shape=(num_examples * example_size), ) corrupt_mm = np.memmap( corrupt_mm_filepath, dtype=np.uint16, mode="w+", shape=(num_examples * example_size), ) mm_write_idx = 0 for batch_idx, batch in enumerate(tqdm(dataloader)): input_audios, corrupt_audios, valid = batch # check for valid audios input_audios = input_audios[valid, ...] corrupt_audios = corrupt_audios[valid, ...] input_audios_list = [] corrupt_audios_list = [] for elem_idx in range(input_audios.shape[0]): input_audios_list.append(input_audios[elem_idx, ...]) corrupt_audios_list.append(corrupt_audios[elem_idx, ...]) # embed input/output with VAE input_embeds = vae_encode(input_audios_list) corrupt_embeds = vae_encode(corrupt_audios_list) # write codes to the respective memmaps for input_embed, corrupt_embed in zip(input_embeds, corrupt_embeds): # check for nan in embeds # if np.any(np.isnan(input_embed)): # print("Warning: found NaN in input_embed. Skipping..") # continue # if np.any(np.isnan(corrupt_embed)): # print("Warning: found NaN in corrupt_embed. Skipping...") # continue input_mm[mm_write_idx : mm_write_idx + example_size] = np.reshape( input_embed, (-1) ) corrupt_mm[mm_write_idx : mm_write_idx + example_size] = np.reshape( corrupt_embed, (-1) ) mm_write_idx += example_size