Stargan-vc2 代码阅读笔记


前言

  非并行多域语音转换(VC)是一种在不依赖于并行数据的情况下学习多个域之间的映射的技术。这是重要的,但具有挑战性,因为需要学习多个映射,并且显式监督不可用。最近,StarGAN-VC因其仅使用单个生成器即可解决此问题而备受关注。然而,真实语音和转换语音之间仍然存在差距。为了弥补这一差距,我们重新思考了StarGAN-VC的条件方法,这是在单个模型中实现非并行多域VC的关键组成部分,并提出了一种改进的变体,称为StarGAN-VC2。特别是,我们从两个方面重新思考了条件方法:训练目标和网络架构。对于前者,我们提出了一种源和目标条件对抗性损失,允许所有源域数据转换为目标域数据。对于后者,我们介绍了一种基于调制的条件方法,该方法可以以特定领域的方式转换声学特征的调制。我们在非并行多说话人VC上评估了我们的方法。客观评估表明,我们提出的方法在全局和局部结构测量方面都提高了语音质量。此外,主观评估表明,StarGAN-VC2在自然度和说话者相似性方面优于StarGAN-VC。


正文

论文: StarGAN-VC2:Rethinking Conditional Methods for StarGAN-Based Voice Conversion
论文连接

变声器

  • 变声器的工作原理是什么呢?
  • 其实就是把语音特征进行转换,只不过内容不能变!
    在这里插入图片描述

VC:Voice Conversion

  • 如何构建一个变声器呢?思想跟stargan差不多,细节完全不同
  • 需要输入什么?1.声音数据;2.标签编码;
  • 整体来说还是GAN模型,主要解决数据特征提取,网络模型定义
  • stargan-vc2是升级版,前身还有cyclegan-vc和stargan-vc

使用数据集

  • VCC2016
  • 4个人的声音数据,相当于4个domain,他们之间相互转换【‘SF1’,‘SF2’,‘TM1’,‘TM2’】
  • 论文中选择的特征为:MFCCs(梅尔倒谱系数);log F0(基频的对数);APs(声学参数);
  • 论文使用输入特征为:batchsize135*128**(35为特征个数,128为指定特征维度或者切分)**

科普:
  1.梅尔倒谱系数,用于表示语音信号的特性。MFCCs是语音信号处理中常用的一种参数,通过对语音信号进行傅里叶变换和一系列数学处理,得到一组倒谱系数,用于描述语音信号的频谱特性和声道形状。
  2.log F0 表示基频的对数。基频是指声音信号中最低的频率成分,通常与声道的振动频率相关,对于语音信号来说,基频决定了音调的高低。
  3.APs(Acoustic Parameters)是声学参数,用于描述语音信号的声学特性。APs 可以包括多种参数,如频谱参数、能量参数、持续时间参数等,用于分析和表示语音信号的不同特征。

输入数据格式

  • 频率:每秒钟波峰所发生的数目称之为信号的频率,用单位千赫兹(kHz)表示
  • 例如:0.1毫秒完成4.8次采样,则1秒48000次采样,采样率48KHZ

在这里插入图片描述

预处理数据

  • 16KHZ重采样
  • 预加重:补偿高频信号,让高频信号权重更大一些,因为它信息多
  • 分帧:类似时间窗口,得到多个特征段

论文中的特征汇总

  • 基频特征(F0):声音可以分解成不同频率的正弦波,其中最低的那个波谷。
  • 频谱包络:语音是一个时序信号,如采样频率为16kHz的音频文件(每秒包含16000个采样点)分帧后得到了多个子序列,然后对每个子序列进行傅里叶变换操作,就得到了频率-振幅图(也就是描述频率-振幅图变化趋势的)
  • Aperiodic参数:基于F0与频谱包络计算得到

科普:

傅里叶变换的作用

 1. 信号分析:傅里叶变换可以将语音信号从时域转换到频域,从而提供语音信号的频谱信息。这有助于分析和理解语音信号的频率特性。
 2. 特征提取:通过对语音信号进行傅里叶变换,可以提取出语音信号中的频率分量,这些频率分量可以作为语音信号的特征参数,用于后续的语音识别和分类。
 3. 降噪和预处理:傅里叶变换还可以用于语音信号的降噪预处理。通过去除语音信号中的噪声和干扰,可以提高语音识别的准确性和鲁棒性。

Aperiodic参数的作用

  在语音合成中,为了获得更自然的声音,激励源不仅包含周期信号,还需要包含一些非周期信号。Aperiodic参数就是用于描述这种非周期信号的特性,它与周期信号一起共同构成了语音信号的激励源。通过对Aperiodic参数的提取和分析,可以更好地理解和表示语音信号的特性,从而提高语音合成的质量和自然度。

MFCC

 MFCC(Mel Frequency Cepstral Coefficients)梅尔频率倒谱系数。

 流程为:
在这里插入图片描述

 1. 预处理:对原始语音信号进行预处理,包括预加重、分帧和加窗等操作。
 2. 傅里叶变换:对每帧语音信号进行傅里叶变换,得到语音信号的频谱。
 3. 梅尔滤波:将语音信号的频谱映射到梅尔刻度上,并通过一组三角滤波器组进行滤波,得到每个滤波器的输出。
 4. 对数运算和离散余弦变换(DCT):对每个滤波器的输出取对数,并进行离散余弦变换,得到MFCC系数。

科普:
 1. 通俗解释:FFT之后就把语音转换到频域,MEL滤波器变换后相当于得到更符合人类听觉的效果:
在这里插入图片描述

 2. 最后DCT相当于提取每一帧的包络(这里面特征多),包络是指一个信号的最大值和最小值构成的曲线,它反映了信号幅度的变化。在语音信号处理中,包络通常用于描述语音信号的幅度变化特性。

网络架构

  • 生成器:输入就是提取好的特征,输出也就是特征
  • 感觉就是编码-解码的过程,其中引入了IN和GLU单元
    在这里插入图片描述

语音数据包含的成分

在这里插入图片描述
    图中的Content Encoder是提取声音内容;Speaker Encoder是提取声音特征;然后经过解码器Decoder,拼接成一个新的有内容和新特征的声音,进而实现变声器的效果。
思考:

  • 变声器虽然把咱们动静给改了,但是内容没变吧!
  • 编码时如何保留住原始内容呢?这就得去掉声音中特性的部分
  • 解码时如何放大个性呢?还是需要再处理解码特征

Instance Normalization(IN)

  把声音特征去掉,就是声音内容了。
  IN是在每个样本的每个通道上单独进行归一化操作。具体来说,IN计算每个样本每个通道的均值和方差,并使用它们对该通道的数据进行标准化。
在这里插入图片描述

Adaptive Instance Normalization(AdaIn)

  IN是一种用于解决内部协变量偏移问题的归一化方法,它通过对每个通道的激活进行归一化,使得网络的训练更加稳定。然而,IN在风格迁移任务中可能会导致风格的丢失。
  为了解决这个问题,AdaIn被提了出来。AdaIn不仅对每个通道的激活进行归一化,还学习了一个缩放因子和偏移因子,这两个因子是根据数据风格计算得出的。这样,AdaIn就可以将内容的激活分布调整为风格的激活分布,从而实现了风格的迁移。
在这里插入图片描述
在这里插入图片描述

生成器 PS(PixelShuffle)

 不会…

判别器

  1. GSP:global sum pooling:一个特征图压缩成一个点,batch512hw,压缩成batch512的点
  2. 然后,标签通过embeding编码成512维特征(batch*512),内积得到batch个判别结果
    在这里插入图片描述

代码

  1. preprocess.py

这段代码执行了以下操作:

  1. 导入所需的库和模块。
  2. 定义了命令行参数解析器(ArgumentParser)。该解析器用于从命令行输入的参数中解析出指定的参数值。
    • 定义了--dataset参数,用于指定数据集名称,默认值为VCC2016,可选值为VCC2016VCC2018
    • 定义了--input_dir参数,用于指定输入数据的目录,默认值为./data/spk
    • 定义了--output_dir参数,用于指定处理后数据的输出目录,默认值为./data/processed
  3. 解析命令行参数,并将解析后的值保存到相应的变量中。
  4. 创建输出目录(如果不存在)。
  5. 根据选择的数据集,设置采样率。
    • 如果数据集为VCC2016,则采样率为16000 Hz。
    • 如果数据集为VCC2018,则采样率为22050 Hz。
  6. 调用wav_to_mcep_file函数,将输入目录中的音频文件转换为梅尔倒谱系数(MCCs),并保存到输出目录中的文件中。
  7. 实例化GenerateStatistics类的对象,并指定数据集的输出目录。
  8. 调用generate_stats方法,生成统计特征(均值和标准差)。
  9. 调用normalize_dataset方法,对数据集进行归一化处理。
  10. 输出程序执行的持续时间。

  总的来说,这段代码执行了音频数据的预处理过程,包括将音频转换为梅尔倒谱系数(MCCs),计算统计特征,并对数据集进行归一化处理。

if __name__ == "__main__":
    start = datetime.now()
    parser = argparse.ArgumentParser(description='Convert the wav waveform to mel-cepstral coefficients(MCCs)\
    and calculate the speech statistical characteristics.')
    
    input_dir = './data/spk'
    output_dir = './data/processed'

    dataset_default = 'VCC2016'

    parser.add_argument('--dataset', type=str, default=dataset_default, choices=['VCC2016', 'VCC2018'], 
        help='Available datasets: VCC2016 and VCC2018 (Default: VCC2016).')
    parser.add_argument('--input_dir', type=str, default=input_dir, help='Directory of input data.')
    parser.add_argument('--output_dir', type=str, default=output_dir, help='Directory of processed data.')
    
    argv = parser.parse_args()
    input_dir = argv.input_dir
    output_dir = argv.output_dir

    os.makedirs(output_dir, exist_ok=True)

    """
        Sample rate:
            VCC2016: 16000 Hz
            VCC2018: 22050 Hz
    """
    if argv.dataset == 'VCC2016':
        sample_rate = 16000
    else:
        sample_rate = 22050

    wav_to_mcep_file(input_dir, sample_rate, processed_filepath=output_dir)

    # 生成统计特征
    generator = GenerateStatistics(output_dir)
    generator.generate_stats()
    generator.normalize_dataset()
    end = datetime.now()
    
    print(f"* Duration: {
     
     end-start}.")

这段代码定义了一个名为load_wavs的函数,用于加载音频文件并进行预处理。

函数的参数包括:

  • dataset:数据集路径,指定包含音频文件的目录。
  • sr:采样率。

函数的流程如下:

  1. 创建一个空字典data,用于存储音频文件的路径。
  2. 使用os.scandir遍历数据集目录中的文件和子目录。
  3. 对于每个子目录,将其作为键添加到data字典中,并创建一个空列表作为对应的值。
  4. 使用os.scandir遍历子目录中的音频文件。
  5. 对于每个音频文件,将其路径添加到相应子目录的列表中。
  6. 输出加载的键(子目录名称)。
  7. 创建一个空字典resdict,用于存储处理后的音频数据。
  8. 初始化计数器cnt为0。
  9. 遍历data字典的键值对,进行音频文件的预处理。
  10. 对于每个音频文件路径,提取文件名作为新的键。
  11. 使用librosa.load函数加载音频文件,将其转换为浮点数类型的波形数据。
  12. 使用librosa.effects.trim函数对波形数据进行修剪,突出高频信号。
  13. 应用预加重滤波器,将修剪后的波形数据附加到之前的采样点后面。
  14. 将处理后的波形数据添加到resdict字典中对应的子目录和文件键中。
  15. 输出进度点"."(每处理一个音频文件输出一个点)。
  16. 递增计数器cnt
  17. 输出总音频文件数量。
  18. 返回处理后的resdict字典。

  总的来说,这段代码实现了对音频文件的加载和预处理操作。它遍历数据集目录并获取每个子目录中的音频文件路径。然后,它使用Librosa库加载音频文件,对其进行修剪和预加重滤波器处理,并将处理后的音频数据保存在一个字典中。最后,返回包含处理后数据的字典。

# 用于加载音频文件。
def load_wavs(dataset: str, sr):
    """
        `data`: contains all audios file path. 
        `resdict`: contains all wav files.   
    """

    data = {
   
   }
    with os.scandir(dataset) as it:
        for entry in it:
            if entry.is_dir():
                data[entry.name] = []
                with os.scandir(entry.path) as it_f:
                    for onefile in it_f:
                        if onefile.is_file():
                            data[entry.name].append(onefile.path)
    print(f'* Loaded keys: {
     
     data.keys()}')
    resdict = {
   
   }

    cnt = 0
    for key, value in data.items():
        resdict[key] = {
   
   }

        for one_file in value: #预处理,突出高频信号,因为一般发音的话高频信号能表达更多有用的信息
            filename = one_file.split('/')[-1].split('.')[0] 
            newkey = f'{
     
     filename}'
            wav, _ = librosa.load(one_file, sr=sr, mono=True, dtype=np.float64)#sr:采样率 mono:单通道
            y, _ = librosa.effects.trim(wav, top_db=15)
            wav = np.append(y[0], y[1: ] - 0.97 * y[: -1])

            resdict[key][newkey] = wav
            print('.', end='')
            cnt += 1

    print(f'\n* Total audio files: {
     
     cnt}.')
    return resdict

    # 用于生成指定大小的迭代器块。
        # iterable:可迭代对象。
        # size:块大小。
    # 函数功能:
        # 将可迭代对象拆分为指定大小的块。
        # 逐块返回生成器。

这段代码定义了一个名为chunks的函数,用于将可迭代对象按照指定大小拆分成连续的子块。

函数的参数包括:

  • iterable:可迭代对象,即需要拆分的对象。
  • size:块的大小,指定每个子块的元素个数。

函数的流程如下:

  1. 使用range函数生成一个以size为步长的索引序列,遍历这个索引序列。
  2. 在每次迭代中,根据当前索引截取iterable中的连续子序列,子序列从当前索引开始,长度为size
  3. 使用yield语句将每个子序列作为生成器的输出。
  4. 持续迭代直到所有的子块都被生成和返回。

  总的来说,这段代码实现了将一个可迭代对象拆分为连续的子块,每个子块包含指定数量的元素。它通过生成器的方式返回拆分后的子块,可以在需要的时候逐个获取子块,而不是一次性生成和返回所有的子块。

def chunks(iterable, size):
    """
        Yield successive n-sized chunks from iterable.
    """

    for i in range(0, len(iterable), size):
        yield iterable[i: i + size]



    # 用于将音频文件转换为MCEP特征。
    # sr: 采样率。
    # 进行音频文件转换和特征提取。

这段代码定义了一个名为wav_to_mcep_file的函数,用于将音频文件转换为MCEP特征并保存为文件。

函数的参数包括:

  • dataset:数据集路径,指定包含音频文件的目录。
  • sr:采样率。
  • processed_filepath:转换后的特征文件保存路径,默认为"./data/processed"。

函数的流程如下:

  1. 使用shutil.rmtree函数删除已存在的processed_filepath目录及其内容。
  2. 使用os.makedirs函数创建processed_filepath目录,如果目录已存在则不进行任何操作。
  3. 使用glob.glob函数计算数据集中所有音频文件的数量,并输出总音频文件数量。
  4. 调用load_wavs函数加载和预处理音频数据,将结果存储在字典d中。
  5. 遍历字典d中的每个说话者。
  6. 获取当前说话者的音频数据列表values_of_one_speaker
  7. 使用chunks函数将音频数据列表拆分为指定大小的块one_chunk
  8. 初始化一个空列表wav_concated用于存储拼接后的音频数据。
  9. 对于每个块,将其数据拼接到wav_concated列表中。
  10. wav_concated列表转换为NumPy数组wav_concated
  11. 调用cal_mcep函数计算MCEP特征,得到基频(f0)、频谱包络(ap)和梅尔频谱倒谱系数(mcep)。
  12. 根据说话者名称和块索引生成新的名称newname
  13. 构建特征文件路径file_path_z,使用np.savez函数将f0和mcep保存为NPZ文件。
  14. 输出保存特征文件的消息。
  15. 对于每个块中的每个帧,在MCEP矩阵中滑动窗口,每次取连续的FRAMES帧作为一个音频片段。
  16. 如果音频片段的帧数等于FRAMES,则生成临时文件名temp_name并构建保存路径filePath,使用np.save函数将音频片段保存为NPY文件。
  17. 输出保存音频片段文件的消息。

  总的来说,这段代码实现了将音频文件转换为MCEP特征,并将特征数据保存为文件。它遍历数据集中的音频文件,将它们按照指定大小进行拼接,并计算拼接后音频数据的MCEP特征。然后,它将特征数据保存为NPZ格式的文件,以及根据帧窗口滑动的方式,将每个音频片段保存为单独的NPY文件。

def wav_to_mcep_file(dataset: str, sr: int, processed_filepath: str='./data/processed'):
    """
        Convert wavs to MCEPs feature using image representation.
    """

    shutil.rmtree(processed_filepath)
    os.makedirs(processed_filepath, exist_ok=True)

    allwavs_cnt = len(glob.glob(f'{
     
     dataset}/*/*.wav'))
    print(f'* Total audio files: {
     
     allwavs_cnt}.')

    d = load_wavs(dataset, sr)
    for one_speaker in d.keys():
        values_of_one_speaker = list(d[one_speaker].values())
       
        for index, one_chunk in enumerate(chunks(values_of_one_speaker, CHUNK_SIZE)):
            wav_concated = [] 
            temp = one_chunk.copy()

            for one in temp:
                wav_concated.extend(one)
            wav_concated = np.array(wav_concated)

            f0, ap, mcep = cal_mcep(wav_concated, sr, FEATURE_DIM, FFTSIZE, SHIFTMS, ALPHA)
            
            newname = f'{
     
     one_speaker}_{
     
     index}'
            file_path_z = os.path.join(processed_filepath, newname)
            np.savez(file_path_z, f0=f0, mcep=mcep)
            print(f'[SAVE]: {
     
     file_path_z}')

            for start_idx in range(0, mcep.shape[1] - FRAMES + 1, FRAMES):
                one_audio_seg = mcep[:, start_idx: start_idx + FRAMES]

                if one_audio_seg.shape[1] == FRAMES:
                    temp_name = f'{
     
     newname}_{
     
     start_idx}'
                    filePath = os.path.join(processed_filepath, temp_name)
                    np.save(filePath, one_audio_seg)
                    print(f'[SAVE]: {
     
     filePath}.npy')
  1. solver.py

    1. 恢复或加载预训练的模型。
    2. 从指定的测试目录读取数据,包括源说话者的音频数据和说话者标签。
    3. 获取目标说话者列表。
    4. 对于每个目标说话者,进行以下操作:
      • 将源说话者和目标说话者的标签进行编码转换。
      • 对于给定的音频文件和内容,进行以下操作:
        • 提取声音特征,包括基频(f0)、包络(ap)和梅尔倒谱系数(mcep_norm)。
        • 创建一个空列表,用于存储转换结果。
        • 对于每个特征帧,进行以下操作:
          • 将特征帧从源说话者转换为目标说话者。
          • 对转换后的特征进行一些后处理操作。
          • 将转换结果添加到列表中。
        • 将转换结果拼接起来,并进行一些修剪操作。
        • 进行音频合成,生成转换后的音频。
        • 将转换后的音频保存到文件中。
class Solver(object):
    def __init__(self, data_loader, config):
        self.config = config
        self.data_loader = data_loader
        self.num_spk = config.num_spk

        if config.dataset == 'VCC2016':
            self.sample_rate = 16000
        else:
            self.sample_rate = 22050
       
        self.lambda_cyc = config.lambda_cyc
        self.lambda_gp = config.lambda_gp
        self.lambda_id = config.lambda_id

        # Training configurations.
        self.data_dir = config.data_dir
        self.test_dir = config.test_dir
        self.batch_size = config.batch_size
        self.num_iters = config.num_iters
        self.num_iters_decay = config.num_iters_decay
        self.g_lr = config.g_lr
        self.d_lr = config.d_lr
        self.n_critic = config.n_critic
        self.beta1 = config.beta1
        self.beta2 = config.beta2
        self.resume_iters = config.resume_iters
        
        # Test configurations.
        self.test_iters = config.test_iters
        self.trg_speaker = ast.literal_eval(config.trg_speaker)
        self.src_speaker = config.src_speaker

        self.use_tensorboard = config.use_tensorboard
        self.device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
        self.spk_enc = LabelBinarizer().fit(speakers)

        self.log_dir = config.log_dir
        self.sample_dir = config.sample_dir
        self.model_save_dir = config.model_save_dir
        self.result_dir = config.result_dir

        self.log_step = config.log_step
        self.sample_step = config.sample_step
        self.model_save_step = config.model_save_step
        self
评论 7
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值