OpenVoice实时语音克隆功能实现

news2024/12/23 6:17:52

前言

        在【OpenVoice本地部署教程与踩坑记录】一文中介绍了OpenVoice的基本概念与,并且完成了项目的安装与运行。官方给的示例和用法中仅包含了文本转TTS再克隆音色的功能,仅能用于TTS场景下的文字朗读。

        本文基于官方示例改造,实现了实时采集麦克风音频进行语音克隆的功能。在阅读项目论文理论后,少量修改了官方源码,取得了不错的实测效果。

论文

        项目论文可以在这里查看【OpenVoice: Versatile Instant Voice Cloning】,我将原文机器翻译成了中文,也可以参考中文版的论文【百度网盘,提取码: d7ja】。

        阅读论文后可以知道,OpenVoice将 IVC 任务解耦为独立的子任务,与耦合任务相比,每个子任务都更容易实现,音调颜色的克隆与所有其余风格参数和语言的控制完全分离。我们也可以不依赖原有的TTS,可以使用本地录音进行音色转换。在阅读完论文和相关代码后发现大部分为音频数据的基础数学计算,未包含前后强逻辑关联,故我们可以引入Buff的概念,将音频数据一段一段的送入转换,所以实时音频转换从理论上是可行的。

*我不是专业研究员,上述内容如有错误还望不吝赐教

实现

        我们实现的基本思路是利用pyaudio库从麦克风实时采集np.float32的音频数据,然后传入到convert方法中进行转换,将转换后的结果同样通过pyaudio库播放即可。

pyaudio安装

        使用pip下载pyaudio库即可正常使用:

pip install pyaudio

pyaudio基础使用

        关于pyaudio的基础使用可以参考网上的其它文章,这个库用起来不复杂,仅需要简单配置即可,我让chatGPT帮我写了一个测试示例供大家参考:

import pyaudio
import numpy as np

def play_and_record():
    # 设置参数
    FORMAT = pyaudio.paInt16
    CHANNELS = 1  # 单声道
    RATE = 44100  # 采样率,您可以根据需要调整
    CHUNK = 1024  # 每次读取的样本数

    p = pyaudio.PyAudio()

    # 打开麦克风
    stream_in = p.open(format=FORMAT,
                       channels=CHANNELS,
                       rate=RATE,
                       input=True,
                       frames_per_buffer=CHUNK)

    # 打开扬声器
    stream_out = p.open(format=FORMAT,
                        channels=CHANNELS,
                        rate=RATE,
                        output=True,
                        frames_per_buffer=CHUNK)

    print("开始录音并播放...")

    try:
        while True:
            # 从麦克风读取数据
            data = stream_in.read(CHUNK)
            
            # 将数据写入扬声器以播放
            stream_out.write(data)
            
    except KeyboardInterrupt:
        print("停止录音并播放。")
        stream_in.stop_stream()
        stream_in.close()
        stream_out.stop_stream()
        stream_out.close()
        p.terminate()

if __name__ == '__main__':
    play_and_record()

        运行上述代码后,您应该能够从麦克风采集声音并立即通过扬声器播放。请注意,此示例简单地读取和播放声音,没有进行任何处理或分析。

OpenVoice修改

        经过上文的入门后,相信你对OpenVoice的运行方式有了一个基础认识,OpenVoice核心的方法就是通过convert调用转换,要求我们传入源文件、目标输出、相关音色文件等参数,调用的代码看起来像这样:

tone_color_converter.convert(
    audio_src_path=src_path, 
    src_se=source_se, 
    tgt_se=target_se, 
    output_path=save_path,
    message=encode_message)

        我们进入convert方法里面,可以看到是使用librosa库读取音频文件,采样率为22050(hps.data.sampling_rate中定义)。然后转换为torch数据传入后文进行处理,进行上文提到的音频数据的基础数学计算(OpenVoice框架图中的黄色部分),在处理完成后得到audio原始数据,最后进行添加水印的操作后存储到目标文件或直接返回。这里的代码十分清晰明了,完整的还原了框架图中的步骤,我们的改造不必关心底层的处理逻辑,到API这一层已经足够满足功能了。

    def convert(self, audio_src_path, src_se, tgt_se, output_path=None, tau=0.3, message="default"):
        hps = self.hps
        # load audio
        audio, sample_rate = librosa.load(audio_src_path, sr=hps.data.sampling_rate)
        audio = torch.tensor(audio).float()
        
        with torch.no_grad():
            y = torch.FloatTensor(audio).to(self.device)
            y = y.unsqueeze(0)
            spec = spectrogram_torch(y, hps.data.filter_length,
                                    hps.data.sampling_rate, hps.data.hop_length, hps.data.win_length,
                                    center=False).to(self.device)
            spec_lengths = torch.LongTensor([spec.size(-1)]).to(self.device)
            audio = self.model.voice_conversion(spec, spec_lengths, sid_src=src_se, sid_tgt=tgt_se, tau=tau)[0][
                        0, 0].data.cpu().float().numpy()
            audio = self.add_watermark(audio, message)
            if output_path is None:
                return audio
            else:
                soundfile.write(output_path, audio, hps.data.sampling_rate)

        在理解了官方代码后,我们加入断点进行debug查看各个环节的数据流转。librosa库读取出来的是float32的np数组格式的音频数据,最后生成的audio也是float32的np数组格式的音频数据,我们需要保证实时音频满足相应的数据格式。我们仅需完成以下步骤的改造即可:

  1. 使用pyaudio采集22050采样率的float32格式的音频
  2. 将pyaudio读取到的byte数据转为np数组
  3. 修改入参,传入实时音频数据而不是音频文件地址
  4. 取消librosa文件读取相关代码
  5. 取消水印代码,因为加水印对音频时长有要求
  6. 取消文件保存相关代码,直接返回原始音频数组

        我们的核心思路是将一整段音频文件转换的方式变为一小段一小段的实时音频转换的方式,所以需要引入buff的概念,经过实测发现我们的buff长度最好不低于10000(0.9s),这个参数可以在pyaudio中进行配置,完整的代码如下

import pyaudio
import numpy as np
import torch
import se_extractor
from api import ToneColorConverter

ckpt_converter = 'checkpoints/converter'
# 使用CPU推理
device = 'cpu'
tone_color_converter = ToneColorConverter(f'{ckpt_converter}/config.json', device=device)
tone_color_converter.load_ckpt(f'{ckpt_converter}/checkpoint.pth')
# 欲克隆的声音,建议自行录制一段其他人的音频,官方例子是英语,对中文不太友好
reference_speaker = 'resources/example_reference.mp3'
target_se, _ = se_extractor.get_se(reference_speaker, tone_color_converter, target_dir='processed', vad=True)
# 当前说话人的音色文件,可以录音一段然后通过上面的方法生成,在processed文件夹中就可以找到
source_se = torch.load(f'processed/yl3/se.pth').to(device)

def play_and_record():
    # 设置参数
    FORMAT = pyaudio.paFloat32
    CHANNELS = 1  # 单声道
    RATE = 22050  # 采样率,固定
    BUFF = 10000  # 每次读取的样本数,每段的音频长度
    BUFF_OUT = 9984  # 每次播放的样本数,convert后长度有一定变化,避免产生杂音
    p = pyaudio.PyAudio()
    # 打开麦克风
    stream_in = p.open(format=FORMAT,
                       channels=CHANNELS,
                       rate=RATE,
                       input=True,
                       frames_per_buffer=BUFF)
    # 打开扬声器
    stream_out = p.open(format=FORMAT,
                        channels=CHANNELS,
                        rate=RATE,
                        output=True,
                        frames_per_buffer=BUFF_OUT) 
    print("开始录音并播放...")

    try:
        while True:
            # 从麦克风读取数据
            data = stream_in.read(BUFF)
            # 转换为np数组
            data = np.frombuffer(data, dtype=np.float32)
            # 实时变音
            audio = tone_color_converter.convertRealTime(
                audio_data= data, 
                src_se=source_se, 
                tgt_se=target_se)
            # 将数据写入扬声器以播放
            stream_out.write(audio.tobytes())
            
    except KeyboardInterrupt:
        print("停止录音并播放。")
        stream_in.stop_stream()
        stream_in.close()
        stream_out.stop_stream()
        stream_out.close()
        p.terminate()

if __name__ == '__main__':
    play_and_record()

修改api.py文件,加入convertRealTime方法:

def convertRealTime(self, audio_data, src_se, tgt_se, tau=0.3):
    hps = self.hps
    # 重采样,可选
    # audio = librosa.resample(audio_data, orig_sr=16000, target_sr=22050)
    audio = torch.tensor(audio_data).float()

    with torch.no_grad():
        y = torch.FloatTensor(audio).to(self.device)
        y = y.unsqueeze(0)
        spec = spectrogram_torch(y, hps.data.filter_length,
                                hps.data.sampling_rate, hps.data.hop_length, hps.data.win_length,
                                center=False).to(self.device)
        spec_lengths = torch.LongTensor([spec.size(-1)]).to(self.device)
        audio = self.model.voice_conversion(spec, spec_lengths, sid_src=src_se, sid_tgt=tgt_se, tau=tau)[0][
                    0, 0].data.cpu().float().numpy()
        # 直接返回音频数据
        return audio

优化

        如果一切顺利,我相信你已经跑起来了,并可以实时的听到你说出的话被正常采集和变音。不过你会发现CPU利用率会一直很高,因为我们持续不断的在采集和处理麦克风的数据,即使没有任何人在说话。为了解决这一问题,我们需要进行静音检测,在音频能量值(响度)大于某一个阈值时才激活音频转换,这样可以避免不必要的消耗。

        在实现的过程中会发现基于每个音频包的静音检测会有一定的延迟并会导致丢字的现象,原因不难理解,我们上文提到buff的长度未10000,这是一个很大的值,会引入大约0.9s的延迟。如果我们说话的起始音处于buff的靠后位置,这样程序还是会认为我们处于静音状态,直到下一个包才激活,这样就会丢失最开始的一个字。反之亦然,如果结束音处于buff的靠前位置,那么就会丢失末尾的一个字。

        我们需要将10000长度的buff进行拆分,只要存在超过阈值的响度就认为是激活状态,这样就可以避免首字或尾音丢失的问题。在从激活到静音的过程中,我们引入了一个静音包的过渡,这样会更好的保证音频的自然度。

        上述的优化方案很简单,不过实际效果还是很不错,因为时间有限,未进行更深入的研究,希望社区能够共创更优的解决方案。

优化后的完整代码:

import pyaudio
import numpy as np
import torch
import se_extractor
from api import ToneColorConverter

ckpt_converter = 'checkpoints/converter'
# 使用CPU推理
device = 'cpu'
tone_color_converter = ToneColorConverter(f'{ckpt_converter}/config.json', device=device)
tone_color_converter.load_ckpt(f'{ckpt_converter}/checkpoint.pth')
# 欲克隆的声音,建议自行录制一段其他人的音频,官方例子是英语,对中文不太友好
reference_speaker = 'resources/example_reference.mp3'
target_se, _ = se_extractor.get_se(reference_speaker, tone_color_converter, target_dir='processed', vad=True)
# 当前说话人的音色文件,可以录音一段然后通过上面的方法生成,在processed文件夹中就可以找到
source_se = torch.load(f'processed/yl3/se.pth').to(device)

# 上一次静音状态缓存,用于尾音平滑
lastSilence = True
# 静音阈值(根据实际需求调整)
THRESHOLD_ENERGY = 0.05
# 静音检测
def is_silence(frame):
    # 将10000的buff拆分为2000,减少延迟,提高灵敏度
    for i in range(5):
        # 计算能量
        energy = np.sum(frame[i*2000 : (i+1)*2000] ** 2) / len(frame[i*2000 : (i+1)*2000])
        print(energy)
        # 检测音频帧是否为静音
        if energy > THRESHOLD_ENERGY:
            # 只要有一个buff超过阈值就返回不是静音
            return False
    # 是静音
    return True

def play_and_record():
    # 设置参数
    FORMAT = pyaudio.paFloat32
    CHANNELS = 1  # 单声道
    RATE = 22050  # 采样率,固定
    BUFF = 10000  # 每次读取的样本数,每段的音频长度
    BUFF_OUT = 9984  # 每次播放的样本数,convert后长度有一定变化,避免产生杂音
    p = pyaudio.PyAudio()
    # 打开麦克风
    stream_in = p.open(format=FORMAT,
                       channels=CHANNELS,
                       rate=RATE,
                       input=True,
                       frames_per_buffer=BUFF)
    # 打开扬声器
    stream_out = p.open(format=FORMAT,
                        channels=CHANNELS,
                        rate=RATE,
                        output=True,
                        frames_per_buffer=BUFF_OUT) 
    print("开始录音并播放...")

    try:
        while True:
            # 从麦克风读取数据
            data = stream_in.read(BUFF)
            # 转换为np数组
            data = np.frombuffer(data, dtype=np.float32)
            # 当前静音状态
            currSilence = is_silence(data)
            # 当前是静音 且 上次是静音,保证一个包的平滑过渡
            if currSilence and lastSilence:
                print("静音")
            else:
                print("有声音")
                # 实时变音
                audio = tone_color_converter.convertRealTime(
                    audio_data= data, 
                    src_se=source_se, 
                    tgt_se=target_se)
                # 将数据写入扬声器以播放
                stream_out.write(audio.tobytes())
            # 缓存上次状态
            lastSilence = currSilence
            
    except KeyboardInterrupt:
        print("停止录音并播放。")
        stream_in.stop_stream()
        stream_in.close()
        stream_out.stop_stream()
        stream_out.close()
        p.terminate()

if __name__ == '__main__':
    play_and_record()

*如果本文对您有帮助,求三连(点赞、收藏、关注)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1368941.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

公司新买的BI,和金蝶系统配合太默契了

公司一直都用金蝶系统来实现包括财务管理、供应链管理、人力资源管理等多个方面的资源的合理配置和业务流程的自动化。但到了数据分析这块,金蝶系统就明显力不从心,需要一个专业的数据分析工具来接手。财务经理推荐用奥威BI,说这款BI的一大特…

Windows内存管理(一):Windows性能监视器(PerfMon)

一、什么是性能监视器 什么是性能监视器? (What is Performance Monitor? )很多时候,我们的计算机只是停止响应、意外关闭或行为异常。这种行为可能有多种原因,指出确切原因可能会有很大帮助。Windows有一个名为Performance Monitor的工具&…

Python——数据类型转换

# 将数字类型转换成字符串 num_str str(111) print(type(num_str), num_str) \# 将浮点类型转换成字符串 float_str str(12.34) print(type(float_str), float_str) # 将字符串转变成数字 num int("234") print(type(num)) # 将字符串转变成浮点型 num2 float(&q…

(已解决)踩坑spring-session-data-redis包出现sessionId不一致问题

问题:今天在使用spring-session-data-redis的jar包时,出现了本地使用时sessionId是一致的,线上使用的时候sessionid是不一致的。 在网上查了半天资料,知道是其中这个包 DefaultCookieSerializer 出现了问题,但是里面的…

基于Listener实现在线人数监测的简单案例

一、需求 只要有用户登录到服务器,就记录在线用户1。 二、使用到的Listner介绍 1、HttpSessionListener 监听器 当一个HttpSession刚被创建或者失效(invalidate)的时候,将会通知HttpSessionListener监听器。 方法声明功能介绍v…

Vscode设置git账户密码(不需要每次都输入)

在Vscode提交项目代码或者拉取代码的时候,如果每次都需要输入git的账户密码,那么就在终端输入: git config --global credential.helper store 命令 然后执行git pull 提示输入用户密码后,就会缓存; ※注:如…

没有货源是不是就没办法在家做抖店?打包发货怎么完成?解答如下

我是王路飞。 有人问了我一个问题:无货源模式的抖店,自己一个人在家里做不了是吧?毕竟打包发货这些问题怎么解决呢? 店铺要是发货不及时被平台罚款怎么办?产品有质量问题怎么解决呢?店铺一直不出单怎么办…

Unity中URP下深度图的线性转化

文章目录 前言一、_ZBufferParams参数有两组值二、LinearEyeDepth1、使用2、Unity源码推导:3、使用矩阵推导: 三、Linear01Depth1、使用2、Unity源码推导3、数学推导: 前言 在之前的文章中,我们实现了对深度图的使用。因为&#…

阿里云弹性网络接口技术的容器网络基础教程

基于容器的虚拟化是一种虚拟化技术。与虚拟机 (VM) 相比,容器更轻量级,部署更方便。Docker是目前主流的容器引擎,支持Linux、Windows等平台,以及Kubernetes(K8S)、Swarm、Rocket&…

奇偶链表00

题目链接 奇偶链表 题目描述 注意点 在 O(1) 的额外空间复杂度和 O(n) 的时间复杂度下解决这个问题偶数组和奇数组内部的相对顺序应该与输入时保持一致 解答思路 奇数组的头节点是head,偶数组的头节点是head.next,关键是要改变每个节点的next指针及…

[ArkUI开发技巧] 应用的全屏式沉浸适配

引言 在开发应用的过程中,为了使用户聚焦在应用本身,最好对应用进行沉浸适配。先前有一种适配方法,将SystemBarProperties设置成应用页面顶部和底部的颜色,但是这种方法在切换页面的过程中过渡十分僵硬,且应用在小窗模…

数据结构实验3:顺序表的基本操作

目录 一、实验目的 二、实验原理 1. 连续存储空间 2. 元素访问 3. 固定大小 4. 容量管理 5. 动态顺序表 6. 顺序表的插入 7. 顺序表的删除 8. 顺序表的应用 三、实验内容 问题描述 代码 截图 分析 一、实验目的 1、 熟练掌握顺序表结构体的实现。 2、 熟练掌握…

记录汇川:ITP与Autoshop进行仿真连接

1、定义如下程序: 2、ITP新建工程: 3、依次选择,最后修改IP 4、定义两个变量 5、拖一个按钮和一个圈出来,地址绑定:M1 6、地址绑定:Y1 7、PLC启动仿真 8、ITP启动在线模拟器 9、即可实现模拟仿真

Redis 配置(二)

目录 redis 配置 Redis 主从复制 主从复制的作用 主从复制流程 搭建Redis 主从复制 Redis 哨兵模式 哨兵模式的作用 哨兵结构 故障转移机制 主节点的选举 搭建Redis 哨兵模式 Redis 群集模式 集群的作用 Redis集群的数据分片 Redis集群的主从复制模型 搭建R…

拓数派加入 OpenCloudOS 操作系统开源社区,作为成员单位参与社区共建

近日,拓数派签署 CLA(Contributor License Agreement 贡献者许可协议),正式加入 OpenCloudOS 操作系统开源社区。 拓数派(英文名称“OpenPie”)是国内基础数据计算领域的高科技创新企业。作为国内云上数据库和数据计算领域的引领者…

CentOS7部署GitLab-ce-16.7.0-ce.0.el7

文章目录 下载地址上传服务器安装访问配置external_url修改防火墙端口开放 重新加载配置访问GitLab出现502访问错误继续访问gitlab账户和密码修改GitLab常用命令 下载地址 gitlab 下载地址 上传服务器 scp -r C:\Users\xxx.xxxx\Downloads\gitlab-ce-16.7.0-ce.0.el7.x86_64…

黑马苍穹外卖学习Day3

目录 公共字段自动填充问题分析实现思路代码实现 新增菜品需求分析和设计接口设计代码开发开发文件上传接口功能开发 菜品分页查询需求分析和设计代码开发 菜品删除功能需求分析与设计代码实现代码优化 修改菜品需求分析和设计代码实现 公共字段自动填充 问题分析 员工表和分…

因为相信,所以简单,因为简单,所以坚持

因为相信,所以简单;因为简单,所以坚持。今天,我有幸受邀参加了九龙珠集团2023年以《蓄力生长》为主题的年会。在这里,我深刻感受到这两句话不仅是九龙珠集团成长的缩影,也是其不断前进的动力。在企业的经营…

选择智能酒精壁炉,拥抱环保与未来生活

保护环境一直是我们共同的责任和目标,而在这场争取保护环境的斗争中,选择使用智能酒精壁炉而非传统壁炉成为了一种积极的行动。这不仅仅是对环境负责,更是对我们自身生活质量的关照。 传统壁炉与智能酒精壁炉的对比 传统壁炉常常以木柴、煤炭…

如何创建VPC并配置安全组以保护您的阿里云服务器

将您的基础架构放在云上意味着您可以接触到全球的许多人。但是,这也意味着不怀好意的人可以访问您的服务。保护您的云网络非常重要。阿里云提供虚拟专用网络 (VPC),这是一个安全隔离的私有云,将您的弹性计算服务 &…