mosibi/RVC_HFv2
0
1import os, sys, traceback, re2 3import json4 5now_dir = os.getcwd()6sys.path.append(now_dir)7from configs.config import Config8 9Config = Config()10import PySimpleGUI as sg11import sounddevice as sd12import noisereduce as nr13import numpy as np14from fairseq import checkpoint_utils15import librosa, torch, pyworld, faiss, time, threading16import torch.nn.functional as F17import torchaudio.transforms as tat18import scipy.signal as signal19import torchcrepe20 21# import matplotlib.pyplot as plt22from lib.infer_pack.models import (23 SynthesizerTrnMs256NSFsid,24 SynthesizerTrnMs256NSFsid_nono,25 SynthesizerTrnMs768NSFsid,26 SynthesizerTrnMs768NSFsid_nono,27)28from i18n import I18nAuto29 30i18n = I18nAuto()31device = torch.device("cuda" if torch.cuda.is_available() else "cpu")32current_dir = os.getcwd()33 34 35class RVC:36 def __init__(37 self, key, f0_method, hubert_path, pth_path, index_path, npy_path, index_rate38 ) -> None:39 """40 初始化41 """42 try:43 self.f0_up_key = key44 self.time_step = 160 / 16000 * 100045 self.f0_min = 5046 self.f0_max = 110047 self.f0_mel_min = 1127 * np.log(1 + self.f0_min / 700)48 self.f0_mel_max = 1127 * np.log(1 + self.f0_max / 700)49 self.f0_method = f0_method50 self.sr = 1600051 self.window = 16052 53 # Get Torch Device54 if torch.cuda.is_available():55 self.torch_device = torch.device(56 f"cuda:{0 % torch.cuda.device_count()}"57 )58 elif torch.backends.mps.is_available():59 self.torch_device = torch.device("mps")60 else:61 self.torch_device = torch.device("cpu")62 63 if index_rate != 0:64 self.index = faiss.read_index(index_path)65 # self.big_npy = np.load(npy_path)66 self.big_npy = self.index.reconstruct_n(0, self.index.ntotal)67 print("index search enabled")68 self.index_rate = index_rate69 model_path = hubert_path70 print("load model(s) from {}".format(model_path))71 models, saved_cfg, task = checkpoint_utils.load_model_ensemble_and_task(72 [model_path],73 suffix="",74 )75 self.model = models[0]76 self.model = self.model.to(device)77 if Config.is_half:78 self.model = self.model.half()79 else:80 self.model = self.model.float()81 self.model.eval()82 cpt = torch.load(pth_path, map_location="cpu")83 self.tgt_sr = cpt["config"][-1]84 cpt["config"][-3] = cpt["weight"]["emb_g.weight"].shape[0] # n_spk85 self.if_f0 = cpt.get("f0", 1)86 self.version = cpt.get("version", "v1")87 if self.version == "v1":88 if self.if_f0 == 1:89 self.net_g = SynthesizerTrnMs256NSFsid(90 *cpt["config"], is_half=Config.is_half91 )92 else:93 self.net_g = SynthesizerTrnMs256NSFsid_nono(*cpt["config"])94 elif self.version == "v2":95 if self.if_f0 == 1:96 self.net_g = SynthesizerTrnMs768NSFsid(97 *cpt["config"], is_half=Config.is_half98 )99 else:100 self.net_g = SynthesizerTrnMs768NSFsid_nono(*cpt["config"])101 del self.net_g.enc_q102 print(self.net_g.load_state_dict(cpt["weight"], strict=False))103 self.net_g.eval().to(device)104 if Config.is_half:105 self.net_g = self.net_g.half()106 else:107 self.net_g = self.net_g.float()108 except:109 print(traceback.format_exc())110 111 def get_regular_crepe_computation(self, x, f0_min, f0_max, model="full"):112 batch_size = 512113 # Compute pitch using first gpu114 audio = torch.tensor(np.copy(x))[None].float()115 f0, pd = torchcrepe.predict(116 audio,117 self.sr,118 self.window,119 f0_min,120 f0_max,121 model,122 batch_size=batch_size,123 device=self.torch_device,124 return_periodicity=True,125 )126 pd = torchcrepe.filter.median(pd, 3)127 f0 = torchcrepe.filter.mean(f0, 3)128 f0[pd < 0.1] = 0129 f0 = f0[0].cpu().numpy()130 return f0131 132 def get_harvest_computation(self, x, f0_min, f0_max):133 f0, t = pyworld.harvest(134 x.astype(np.double),135 fs=self.sr,136 f0_ceil=f0_max,137 f0_floor=f0_min,138 frame_period=10,139 )140 f0 = pyworld.stonemask(x.astype(np.double), f0, t, self.sr)141 f0 = signal.medfilt(f0, 3)142 return f0143 144 def get_f0(self, x, f0_up_key, inp_f0=None):145 # Calculate Padding and f0 details here146 p_len = x.shape[0] // 512 # For Now This probs doesn't work147 x_pad = 1148 f0_min = 50149 f0_max = 1100150 f0_mel_min = 1127 * np.log(1 + f0_min / 700)151 f0_mel_max = 1127 * np.log(1 + f0_max / 700)152 153 f0 = 0154 # Here, check f0_methods and get their computations155 if self.f0_method == "harvest":156 f0 = self.get_harvest_computation(x, f0_min, f0_max)157 elif self.f0_method == "reg-crepe":158 f0 = self.get_regular_crepe_computation(x, f0_min, f0_max)159 elif self.f0_method == "reg-crepe-tiny":160 f0 = self.get_regular_crepe_computation(x, f0_min, f0_max, "tiny")161 162 # Calculate f0_course and f0_bak here163 f0 *= pow(2, f0_up_key / 12)164 # with open("test.txt","w")as f:f.write("\n".join([str(i)for i in f0.tolist()]))165 tf0 = self.sr // self.window # 每秒f0点数166 if inp_f0 is not None:167 delta_t = np.round(168 (inp_f0[:, 0].max() - inp_f0[:, 0].min()) * tf0 + 1169 ).astype("int16")170 replace_f0 = np.interp(171 list(range(delta_t)), inp_f0[:, 0] * 100, inp_f0[:, 1]172 )173 shape = f0[x_pad * tf0 : x_pad * tf0 + len(replace_f0)].shape[0]174 f0[x_pad * tf0 : x_pad * tf0 + len(replace_f0)] = replace_f0[:shape]175 # with open("test_opt.txt","w")as f:f.write("\n".join([str(i)for i in f0.tolist()]))176 f0bak = f0.copy()177 f0_mel = 1127 * np.log(1 + f0 / 700)178 f0_mel[f0_mel > 0] = (f0_mel[f0_mel > 0] - f0_mel_min) * 254 / (179 f0_mel_max - f0_mel_min180 ) + 1181 f0_mel[f0_mel <= 1] = 1182 f0_mel[f0_mel > 255] = 255183 f0_coarse = np.rint(f0_mel).astype(np.int)184 return f0_coarse, f0bak # 1-0185 186 def infer(self, feats: torch.Tensor) -> np.ndarray:187 """188 推理函数189 """190 audio = feats.clone().cpu().numpy()191 assert feats.dim() == 1, feats.dim()192 feats = feats.view(1, -1)193 padding_mask = torch.BoolTensor(feats.shape).fill_(False)194 if Config.is_half:195 feats = feats.half()196 else:197 feats = feats.float()198 inputs = {199 "source": feats.to(device),200 "padding_mask": padding_mask.to(device),201 "output_layer": 9 if self.version == "v1" else 12,202 }203 torch.cuda.synchronize()204 with torch.no_grad():205 logits = self.model.extract_features(**inputs)206 feats = (207 self.model.final_proj(logits[0]) if self.version == "v1" else logits[0]208 )209 210 ####索引优化211 try:212 if (213 hasattr(self, "index")214 and hasattr(self, "big_npy")215 and self.index_rate != 0216 ):217 npy = feats[0].cpu().numpy().astype("float32")218 score, ix = self.index.search(npy, k=8)219 weight = np.square(1 / score)220 weight /= weight.sum(axis=1, keepdims=True)221 npy = np.sum(self.big_npy[ix] * np.expand_dims(weight, axis=2), axis=1)222 if Config.is_half:223 npy = npy.astype("float16")224 feats = (225 torch.from_numpy(npy).unsqueeze(0).to(device) * self.index_rate226 + (1 - self.index_rate) * feats227 )228 else:229 print("index search FAIL or disabled")230 except:231 traceback.print_exc()232 print("index search FAIL")233 feats = F.interpolate(feats.permute(0, 2, 1), scale_factor=2).permute(0, 2, 1)234 torch.cuda.synchronize()235 print(feats.shape)236 if self.if_f0 == 1:237 pitch, pitchf = self.get_f0(audio, self.f0_up_key)238 p_len = min(feats.shape[1], 13000, pitch.shape[0]) # 太大了爆显存239 else:240 pitch, pitchf = None, None241 p_len = min(feats.shape[1], 13000) # 太大了爆显存242 torch.cuda.synchronize()243 # print(feats.shape,pitch.shape)244 feats = feats[:, :p_len, :]245 if self.if_f0 == 1:246 pitch = pitch[:p_len]247 pitchf = pitchf[:p_len]248 pitch = torch.LongTensor(pitch).unsqueeze(0).to(device)249 pitchf = torch.FloatTensor(pitchf).unsqueeze(0).to(device)250 p_len = torch.LongTensor([p_len]).to(device)251 ii = 0 # sid252 sid = torch.LongTensor([ii]).to(device)253 with torch.no_grad():254 if self.if_f0 == 1:255 infered_audio = (256 self.net_g.infer(feats, p_len, pitch, pitchf, sid)[0][0, 0]257 .data.cpu()258 .float()259 )260 else:261 infered_audio = (262 self.net_g.infer(feats, p_len, sid)[0][0, 0].data.cpu().float()263 )264 torch.cuda.synchronize()265 return infered_audio266 267 268class GUIConfig:269 def __init__(self) -> None:270 self.hubert_path: str = ""271 self.pth_path: str = ""272 self.index_path: str = ""273 self.npy_path: str = ""274 self.f0_method: str = ""275 self.pitch: int = 12276 self.samplerate: int = 44100277 self.block_time: float = 1.0 # s278 self.buffer_num: int = 1279 self.threhold: int = -30280 self.crossfade_time: float = 0.08281 self.extra_time: float = 0.04282 self.I_noise_reduce = False283 self.O_noise_reduce = False284 self.index_rate = 0.3285 286 287class GUI:288 def __init__(self) -> None:289 self.config = GUIConfig()290 self.flag_vc = False291 292 self.launcher()293 294 def load(self):295 (296 input_devices,297 output_devices,298 input_devices_indices,299 output_devices_indices,300 ) = self.get_devices()301 try:302 with open("values1.json", "r") as j:303 data = json.load(j)304 except:305 # Injecting f0_method into the json data306 with open("values1.json", "w") as j:307 data = {308 "pth_path": "",309 "index_path": "",310 "sg_input_device": input_devices[311 input_devices_indices.index(sd.default.device[0])312 ],313 "sg_output_device": output_devices[314 output_devices_indices.index(sd.default.device[1])315 ],316 "threhold": "-45",317 "pitch": "0",318 "index_rate": "0",319 "block_time": "1",320 "crossfade_length": "0.04",321 "extra_time": "1",322 }323 return data324 325 def launcher(self):326 data = self.load()327 sg.theme("DarkTeal12")328 input_devices, output_devices, _, _ = self.get_devices()329 layout = [330 [331 sg.Frame(332 title="Proudly forked by Mangio621",333 ),334 sg.Frame(335 title=i18n("Load model"),336 layout=[337 [338 sg.Input(339 default_text="hubert_base.pt",340 key="hubert_path",341 disabled=True,342 ),343 sg.FileBrowse(344 i18n("Hubert Model"),345 initial_folder=os.path.join(os.getcwd()),346 file_types=(("pt files", "*.pt"),),347 ),348 ],349 [350 sg.Input(351 default_text=data.get("pth_path", ""),352 key="pth_path",353 ),354 sg.FileBrowse(355 i18n("Select the .pth file"),356 initial_folder=os.path.join(os.getcwd(), "weights"),357 file_types=(("weight files", "*.pth"),),358 ),359 ],360 [361 sg.Input(362 default_text=data.get("index_path", ""),363 key="index_path",364 ),365 sg.FileBrowse(366 i18n("Select the .index file"),367 initial_folder=os.path.join(os.getcwd(), "logs"),368 file_types=(("index files", "*.index"),),369 ),370 ],371 [372 sg.Input(373 default_text="你不需要填写这个You don't need write this.",374 key="npy_path",375 disabled=True,376 ),377 sg.FileBrowse(378 i18n("Select the .npy file"),379 initial_folder=os.path.join(os.getcwd(), "logs"),380 file_types=(("feature files", "*.npy"),),381 ),382 ],383 ],384 ),385 ],386 [387 # Mangio f0 Selection frame Here388 sg.Frame(389 layout=[390 [391 sg.Radio(392 "Harvest", "f0_method", key="harvest", default=True393 ),394 sg.Radio("Crepe", "f0_method", key="reg-crepe"),395 sg.Radio("Crepe Tiny", "f0_method", key="reg-crepe-tiny"),396 ]397 ],398 title="Select an f0 Method",399 )400 ],401 [402 sg.Frame(403 layout=[404 [405 sg.Text(i18n("Input device")),406 sg.Combo(407 input_devices,408 key="sg_input_device",409 default_value=data.get("sg_input_device", ""),410 ),411 ],412 [413 sg.Text(i18n("Output device")),414 sg.Combo(415 output_devices,416 key="sg_output_device",417 default_value=data.get("sg_output_device", ""),418 ),419 ],420 ],421 title=i18n("Audio device (please use the same type of driver)"),422 )423 ],424 [425 sg.Frame(426 layout=[427 [428 sg.Text(i18n("Response threshold")),429 sg.Slider(430 range=(-60, 0),431 key="threhold",432 resolution=1,433 orientation="h",434 default_value=data.get("threhold", ""),435 ),436 ],437 [438 sg.Text(i18n("Pitch settings")),439 sg.Slider(440 range=(-24, 24),441 key="pitch",442 resolution=1,443 orientation="h",444 default_value=data.get("pitch", ""),445 ),446 ],447 [448 sg.Text(i18n("Index Rate")),449 sg.Slider(450 range=(0.0, 1.0),451 key="index_rate",452 resolution=0.01,453 orientation="h",454 default_value=data.get("index_rate", ""),455 ),456 ],457 ],458 title=i18n("General settings"),459 ),460 sg.Frame(461 layout=[462 [463 sg.Text(i18n("Sample length")),464 sg.Slider(465 range=(0.1, 3.0),466 key="block_time",467 resolution=0.1,468 orientation="h",469 default_value=data.get("block_time", ""),470 ),471 ],472 [473 sg.Text(i18n("Fade length")),474 sg.Slider(475 range=(0.01, 0.15),476 key="crossfade_length",477 resolution=0.01,478 orientation="h",479 default_value=data.get("crossfade_length", ""),480 ),481 ],482 [483 sg.Text(i18n("Extra推理时长")),484 sg.Slider(485 range=(0.05, 3.00),486 key="extra_time",487 resolution=0.01,488 orientation="h",489 default_value=data.get("extra_time", ""),490 ),491 ],492 [493 sg.Checkbox(i18n("Input noise reduction"), key="I_noise_reduce"),494 sg.Checkbox(i18n("Output noise reduction"), key="O_noise_reduce"),495 ],496 ],497 title=i18n("Performance settings"),498 ),499 ],500 [501 sg.Button(i18n("开始音频Convert"), key="start_vc"),502 sg.Button(i18n("停止音频Convert"), key="stop_vc"),503 sg.Text(i18n("Inference time (ms):")),504 sg.Text("0", key="infer_time"),505 ],506 ]507 self.window = sg.Window("RVC - GUI", layout=layout)508 self.event_handler()509 510 def event_handler(self):511 while True:512 event, values = self.window.read()513 if event == sg.WINDOW_CLOSED:514 self.flag_vc = False515 exit()516 if event == "start_vc" and self.flag_vc == False:517 if self.set_values(values) == True:518 print("using_cuda:" + str(torch.cuda.is_available()))519 self.start_vc()520 settings = {521 "pth_path": values["pth_path"],522 "index_path": values["index_path"],523 "f0_method": self.get_f0_method_from_radios(values),524 "sg_input_device": values["sg_input_device"],525 "sg_output_device": values["sg_output_device"],526 "threhold": values["threhold"],527 "pitch": values["pitch"],528 "index_rate": values["index_rate"],529 "block_time": values["block_time"],530 "crossfade_length": values["crossfade_length"],531 "extra_time": values["extra_time"],532 }533 with open("values1.json", "w") as j:534 json.dump(settings, j)535 if event == "stop_vc" and self.flag_vc == True:536 self.flag_vc = False537 538 # Function that returns the used f0 method in string format "harvest"539 def get_f0_method_from_radios(self, values):540 f0_array = [541 {"name": "harvest", "val": values["harvest"]},542 {"name": "reg-crepe", "val": values["reg-crepe"]},543 {"name": "reg-crepe-tiny", "val": values["reg-crepe-tiny"]},544 ]545 # Filter through to find a true value546 used_f0 = ""547 for f0 in f0_array:548 if f0["val"] == True:549 used_f0 = f0["name"]550 break551 if used_f0 == "":552 used_f0 = "harvest" # Default Harvest if used_f0 is empty somehow553 return used_f0554 555 def set_values(self, values):556 if len(values["pth_path"].strip()) == 0:557 sg.popup(i18n("Select the pth file"))558 return False559 if len(values["index_path"].strip()) == 0:560 sg.popup(i18n("Select the index file"))561 return False562 pattern = re.compile("[^\x00-\x7F]+")563 if pattern.findall(values["hubert_path"]):564 sg.popup(i18n("The hubert model path must not contain Chinese characters"))565 return False566 if pattern.findall(values["pth_path"]):567 sg.popup(i18n("The pth file path must not contain Chinese characters."))568 return False569 if pattern.findall(values["index_path"]):570 sg.popup(i18n("The index file path must not contain Chinese characters."))571 return False572 self.set_devices(values["sg_input_device"], values["sg_output_device"])573 self.config.hubert_path = os.path.join(current_dir, "hubert_base.pt")574 self.config.pth_path = values["pth_path"]575 self.config.index_path = values["index_path"]576 self.config.npy_path = values["npy_path"]577 self.config.f0_method = self.get_f0_method_from_radios(values)578 self.config.threhold = values["threhold"]579 self.config.pitch = values["pitch"]580 self.config.block_time = values["block_time"]581 self.config.crossfade_time = values["crossfade_length"]582 self.config.extra_time = values["extra_time"]583 self.config.I_noise_reduce = values["I_noise_reduce"]584 self.config.O_noise_reduce = values["O_noise_reduce"]585 self.config.index_rate = values["index_rate"]586 return True587 588 def start_vc(self):589 torch.cuda.empty_cache()590 self.flag_vc = True591 self.block_frame = int(self.config.block_time * self.config.samplerate)592 self.crossfade_frame = int(self.config.crossfade_time * self.config.samplerate)593 self.sola_search_frame = int(0.012 * self.config.samplerate)594 self.delay_frame = int(0.01 * self.config.samplerate) # 往前预留0.02s595 self.extra_frame = int(self.config.extra_time * self.config.samplerate)596 self.rvc = None597 self.rvc = RVC(598 self.config.pitch,599 self.config.f0_method,600 self.config.hubert_path,601 self.config.pth_path,602 self.config.index_path,603 self.config.npy_path,604 self.config.index_rate,605 )606 self.input_wav: np.ndarray = np.zeros(607 self.extra_frame608 + self.crossfade_frame609 + self.sola_search_frame610 + self.block_frame,611 dtype="float32",612 )613 self.output_wav: torch.Tensor = torch.zeros(614 self.block_frame, device=device, dtype=torch.float32615 )616 self.sola_buffer: torch.Tensor = torch.zeros(617 self.crossfade_frame, device=device, dtype=torch.float32618 )619 self.fade_in_window: torch.Tensor = torch.linspace(620 0.0, 1.0, steps=self.crossfade_frame, device=device, dtype=torch.float32621 )622 self.fade_out_window: torch.Tensor = 1 - self.fade_in_window623 self.resampler1 = tat.Resample(624 orig_freq=self.config.samplerate, new_freq=16000, dtype=torch.float32625 )626 self.resampler2 = tat.Resample(627 orig_freq=self.rvc.tgt_sr,628 new_freq=self.config.samplerate,629 dtype=torch.float32,630 )631 thread_vc = threading.Thread(target=self.soundinput)632 thread_vc.start()633 634 def soundinput(self):635 """636 接受音频输入637 """638 with sd.Stream(639 channels=2,640 callback=self.audio_callback,641 blocksize=self.block_frame,642 samplerate=self.config.samplerate,643 dtype="float32",644 ):645 while self.flag_vc:646 time.sleep(self.config.block_time)647 print("Audio block passed.")648 print("ENDing VC")649 650 def audio_callback(651 self, indata: np.ndarray, outdata: np.ndarray, frames, times, status652 ):653 """654 音频处理655 """656 start_time = time.perf_counter()657 indata = librosa.to_mono(indata.T)658 if self.config.I_noise_reduce:659 indata[:] = nr.reduce_noise(y=indata, sr=self.config.samplerate)660 661 """noise gate"""662 frame_length = 2048663 hop_length = 1024664 rms = librosa.feature.rms(665 y=indata, frame_length=frame_length, hop_length=hop_length666 )667 db_threhold = librosa.amplitude_to_db(rms, ref=1.0)[0] < self.config.threhold668 # print(rms.shape,db.shape,db)669 for i in range(db_threhold.shape[0]):670 if db_threhold[i]:671 indata[i * hop_length : (i + 1) * hop_length] = 0672 self.input_wav[:] = np.append(self.input_wav[self.block_frame :], indata)673 674 # infer675 print("input_wav:" + str(self.input_wav.shape))676 # print('infered_wav:'+str(infer_wav.shape))677 infer_wav: torch.Tensor = self.resampler2(678 self.rvc.infer(self.resampler1(torch.from_numpy(self.input_wav)))679 )[-self.crossfade_frame - self.sola_search_frame - self.block_frame :].to(680 device681 )682 print("infer_wav:" + str(infer_wav.shape))683 684 # SOLA algorithm from https://github.com/yxlllc/DDSP-SVC685 cor_nom = F.conv1d(686 infer_wav[None, None, : self.crossfade_frame + self.sola_search_frame],687 self.sola_buffer[None, None, :],688 )689 cor_den = torch.sqrt(690 F.conv1d(691 infer_wav[None, None, : self.crossfade_frame + self.sola_search_frame]692 ** 2,693 torch.ones(1, 1, self.crossfade_frame, device=device),694 )695 + 1e-8696 )697 sola_offset = torch.argmax(cor_nom[0, 0] / cor_den[0, 0])698 print("sola offset: " + str(int(sola_offset)))699 700 # crossfade701 self.output_wav[:] = infer_wav[sola_offset : sola_offset + self.block_frame]702 self.output_wav[: self.crossfade_frame] *= self.fade_in_window703 self.output_wav[: self.crossfade_frame] += self.sola_buffer[:]704 if sola_offset < self.sola_search_frame:705 self.sola_buffer[:] = (706 infer_wav[707 -self.sola_search_frame708 - self.crossfade_frame709 + sola_offset : -self.sola_search_frame710 + sola_offset711 ]712 * self.fade_out_window713 )714 else:715 self.sola_buffer[:] = (716 infer_wav[-self.crossfade_frame :] * self.fade_out_window717 )718 719 if self.config.O_noise_reduce:720 outdata[:] = np.tile(721 nr.reduce_noise(722 y=self.output_wav[:].cpu().numpy(), sr=self.config.samplerate723 ),724 (2, 1),725 ).T726 else:727 outdata[:] = self.output_wav[:].repeat(2, 1).t().cpu().numpy()728 total_time = time.perf_counter() - start_time729 self.window["infer_time"].update(int(total_time * 1000))730 print("infer time:" + str(total_time))731 print("f0_method: " + str(self.config.f0_method))732 733 def get_devices(self, update: bool = True):734 """获取设备列表"""735 if update:736 sd._terminate()737 sd._initialize()738 devices = sd.query_devices()739 hostapis = sd.query_hostapis()740 for hostapi in hostapis:741 for device_idx in hostapi["devices"]:742 devices[device_idx]["hostapi_name"] = hostapi["name"]743 input_devices = [744 f"{d['name']} ({d['hostapi_name']})"745 for d in devices746 if d["max_input_channels"] > 0747 ]748 output_devices = [749 f"{d['name']} ({d['hostapi_name']})"750 for d in devices751 if d["max_output_channels"] > 0752 ]753 input_devices_indices = [754 d["index"] if "index" in d else d["name"]755 for d in devices756 if d["max_input_channels"] > 0757 ]758 output_devices_indices = [759 d["index"] if "index" in d else d["name"]760 for d in devices761 if d["max_output_channels"] > 0762 ]763 return (764 input_devices,765 output_devices,766 input_devices_indices,767 output_devices_indices,768 )769 770 def set_devices(self, input_device, output_device):771 """设置输出设备"""772 (773 input_devices,774 output_devices,775 input_device_indices,776 output_device_indices,777 ) = self.get_devices()778 sd.default.device[0] = input_device_indices[input_devices.index(input_device)]779 sd.default.device[1] = output_device_indices[780 output_devices.index(output_device)781 ]782 print("input device:" + str(sd.default.device[0]) + ":" + str(input_device))783 print("output device:" + str(sd.default.device[1]) + ":" + str(output_device))784 785 786gui = GUI()787 