's'
This commit is contained in:
@@ -18,6 +18,9 @@ enable_demucs = 是
|
||||
# 是 = 有显卡则用 GPU;否 = 强制 CPU;无显卡时自动回退到 CPU
|
||||
demucs_use_gpu = 是
|
||||
|
||||
# 人声混合比例(0.5-1.0),改善人声自然度,减少尖锐感
|
||||
# 0.85 = 85%人声+15%其他(推荐,更自然);1.0 = 纯人声(可能尖锐)
|
||||
demucs_vocal_mix = 0.85
|
||||
# 人声混合比例(0.5-1.0),改善自然度、减少尖锐
|
||||
# 0.75 = 75%人声+25%混合(推荐);1.0 = 纯人声
|
||||
demucs_vocal_mix = 0.75
|
||||
|
||||
# 混合来源:bass=低音(无混响无重复);other=其他(可能带混响导致重复感)
|
||||
demucs_mix_source = bass
|
||||
@@ -75,10 +75,11 @@ DEMUCS_CHUNK_SEC = 2.0
|
||||
BYTES_PER_SAMPLE = 2
|
||||
|
||||
|
||||
def _demucs_separate_vocals(model, audio_chunk, device, vocal_mix_ratio=1.0):
|
||||
def _demucs_separate_vocals(model, audio_chunk, device, vocal_mix_ratio=1.0, mix_source="bass"):
|
||||
"""
|
||||
对音频块进行人声分离,返回人声轨道。
|
||||
vocal_mix_ratio: 1.0=纯人声;0.85=85%人声+15%other(更自然,减少尖锐感)
|
||||
vocal_mix_ratio: 1.0=纯人声;0.75=75%人声+25%混合(更自然)
|
||||
mix_source: "bass"=与低音混合(少混响无重复感);"other"=与其他混合(可能带混响)
|
||||
"""
|
||||
import torch
|
||||
# audio_chunk: numpy or tensor, shape (channels, samples)
|
||||
@@ -93,17 +94,17 @@ def _demucs_separate_vocals(model, audio_chunk, device, vocal_mix_ratio=1.0):
|
||||
with torch.no_grad():
|
||||
out = apply_model(model, wav, device=device, progress=False, overlap=0.25)
|
||||
# out: (batch, sources, channels, samples)
|
||||
# sources: [drums, bass, other, vocals]
|
||||
# sources: [drums=0, bass=1, other=2, vocals=3]
|
||||
vocals = out[0, 3]
|
||||
if vocal_mix_ratio >= 1.0:
|
||||
return vocals.cpu().numpy()
|
||||
# 混合 other 以保留更自然的人声,减少纯人声的尖锐感
|
||||
other = out[0, 2]
|
||||
mixed = vocals * vocal_mix_ratio + other * (1.0 - vocal_mix_ratio)
|
||||
# 混合 bass 或 other:bass 无混响、无重复感;other 可能带混响导致"重复一遍"
|
||||
blend = out[0, 1] if mix_source == "bass" else out[0, 2]
|
||||
mixed = vocals * vocal_mix_ratio + blend * (1.0 - vocal_mix_ratio)
|
||||
return mixed.cpu().numpy()
|
||||
|
||||
|
||||
def _run_demucs_worker(audio_read_pipe, audio_write_socket, model, device, stop_event, vocal_mix_ratio=1.0):
|
||||
def _run_demucs_worker(audio_read_pipe, audio_write_socket, model, device, stop_event, vocal_mix_ratio=1.0, mix_source="bass"):
|
||||
"""Demucs 工作线程:从 FFmpeg 读音频,分离人声,写入 socket"""
|
||||
import numpy as np
|
||||
chunk_samples = int(DEMUCS_SAMPLE_RATE * DEMUCS_CHUNK_SEC * DEMUCS_CHANNELS)
|
||||
@@ -131,7 +132,7 @@ def _run_demucs_worker(audio_read_pipe, audio_write_socket, model, device, stop_
|
||||
arr = np.frombuffer(chunk, dtype=np.int16)
|
||||
arr = arr.reshape(-1, DEMUCS_CHANNELS).T.astype(np.float32) / 32768.0
|
||||
try:
|
||||
vocals = _demucs_separate_vocals(model, arr, device, vocal_mix_ratio)
|
||||
vocals = _demucs_separate_vocals(model, arr, device, vocal_mix_ratio, mix_source)
|
||||
# float -> s16le
|
||||
out = (vocals * 32768).astype(np.int16).tobytes()
|
||||
try:
|
||||
@@ -150,7 +151,7 @@ def _run_demucs_worker(audio_read_pipe, audio_write_socket, model, device, stop_
|
||||
try:
|
||||
arr = np.frombuffer(buf, dtype=np.int16)
|
||||
arr = arr.reshape(-1, DEMUCS_CHANNELS).T.astype(np.float32) / 32768.0
|
||||
vocals = _demucs_separate_vocals(model, arr, device, vocal_mix_ratio)
|
||||
vocals = _demucs_separate_vocals(model, arr, device, vocal_mix_ratio, mix_source)
|
||||
out = (vocals * 32768).astype(np.int16).tobytes()
|
||||
audio_write_socket.sendall(out)
|
||||
except Exception:
|
||||
@@ -178,7 +179,8 @@ def run_demucs_pipeline(
|
||||
stream_ended_exception,
|
||||
cleanup_proc_fn,
|
||||
stderr_reader_fn,
|
||||
vocal_mix_ratio: float = 0.85,
|
||||
vocal_mix_ratio: float = 0.75,
|
||||
mix_source: str = "bass",
|
||||
stats_interval_sec: int = 10,
|
||||
):
|
||||
"""
|
||||
@@ -194,8 +196,9 @@ def run_demucs_pipeline(
|
||||
|
||||
device = next(model.parameters()).device
|
||||
device_str = "cuda" if "cuda" in str(device) else "cpu"
|
||||
mix_info = "纯人声" if vocal_mix_ratio >= 1.0 else f"{int(vocal_mix_ratio*100)}%人声+{int((1-vocal_mix_ratio)*100)}%其他"
|
||||
print(f"[INFO] Demucs 人声分离已启用 ({device_str}),模式: {mix_info}")
|
||||
src_name = "低音" if mix_source == "bass" else "其他"
|
||||
mix_info = "纯人声" if vocal_mix_ratio >= 1.0 else f"{int(vocal_mix_ratio*100)}%人声+{int((1-vocal_mix_ratio)*100)}%{src_name}"
|
||||
print(f"[INFO] Demucs 人声分离已启用 ({device_str}),模式: {mix_info},低通 9kHz 降尖锐")
|
||||
|
||||
# TCP 服务器用于音频(跨平台)
|
||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
@@ -271,7 +274,7 @@ def run_demucs_pipeline(
|
||||
"-x264-params", "force-cfr=1:scenecut=0:open_gop=0:sync-lookahead=0",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-c:a", "aac", "-b:a", "128k", "-ar", "48000", "-ac", "2",
|
||||
"-af", "aresample=async=1:first_pts=0",
|
||||
"-af", "lowpass=f=9000,aresample=async=1:first_pts=0",
|
||||
"-flags", "+global_header", "-flvflags", "no_duration_filesize",
|
||||
"-f", "flv", youtube_rtmp,
|
||||
]
|
||||
@@ -339,7 +342,7 @@ def run_demucs_pipeline(
|
||||
# 注意:proc_a 输出到 stdout,我们读。conn 是 FFmpeg 作为 client 连上来的,所以 FFmpeg 在读,我们写 conn。
|
||||
demucs_thread = threading.Thread(
|
||||
target=_run_demucs_worker,
|
||||
args=(proc_a.stdout, conn[0], model, device, stop_event, vocal_mix_ratio),
|
||||
args=(proc_a.stdout, conn[0], model, device, stop_event, vocal_mix_ratio, mix_source),
|
||||
daemon=True,
|
||||
)
|
||||
demucs_thread.start()
|
||||
|
||||
@@ -104,7 +104,8 @@ def start_douyin_youtube_ffplay(
|
||||
|
||||
enable_demucs = False
|
||||
demucs_use_gpu = True
|
||||
demucs_vocal_mix = 0.85 # 0.85=人声更自然,1.0=纯人声(可能尖锐)
|
||||
demucs_vocal_mix = 0.75 # 0.75=更自然少尖锐;1.0=纯人声
|
||||
demucs_mix_source = "bass" # bass=无混响无重复;other=可能带混响
|
||||
video_bitrate_mbps = 4.5 # 优先画质,YouTube 720p 推荐 4-4.5
|
||||
stats_interval_sec = 10 # 推流状态日志间隔
|
||||
if os.path.exists(config_file):
|
||||
@@ -125,6 +126,9 @@ def start_douyin_youtube_ffplay(
|
||||
demucs_vocal_mix = v
|
||||
except ValueError:
|
||||
pass
|
||||
elif key_lower == "demucs_mix_source":
|
||||
if val.lower() in ("bass", "other"):
|
||||
demucs_mix_source = val.lower()
|
||||
elif key_lower == "video_bitrate_mbps":
|
||||
try:
|
||||
v = float(val)
|
||||
@@ -145,7 +149,7 @@ def start_douyin_youtube_ffplay(
|
||||
# 兼容:若上面未匹配到 key,取第一个非配置项的值作为 stream key
|
||||
if youtube_rtmp == rtmp_base + default_key:
|
||||
for k, v in config.items("youtube"):
|
||||
if k.strip().lower() not in ("enable_demucs", "demucs_use_gpu", "demucs_vocal_mix", "video_bitrate_mbps", "stats_interval_sec") and v.strip():
|
||||
if k.strip().lower() not in ("enable_demucs", "demucs_use_gpu", "demucs_vocal_mix", "demucs_mix_source", "video_bitrate_mbps", "stats_interval_sec") and v.strip():
|
||||
v = v.strip()
|
||||
youtube_rtmp = v if v.startswith("rtmp://") else rtmp_base + v
|
||||
print(f"[INFO] 从 {config_file} 读取 YouTube stream key: {youtube_rtmp}")
|
||||
@@ -532,6 +536,7 @@ def start_douyin_youtube_ffplay(
|
||||
cleanup_proc_fn=cleanup_proc,
|
||||
stderr_reader_fn=_stderr_reader_thread,
|
||||
vocal_mix_ratio=demucs_vocal_mix,
|
||||
mix_source=demucs_mix_source,
|
||||
stats_interval_sec=stats_interval_sec,
|
||||
)
|
||||
except StreamEnded:
|
||||
|
||||
Reference in New Issue
Block a user