一鍵分離人聲和伴奏 Demucs
想把聖歌隊影音裡的人聲和鋼琴伴奏分離,只要一份腳本就搞定!

分離人聲與伴奏

終端機 真神奇, 可以完成許多令人 意想不到 的事, 要把YouTube影音的人聲與鋼琴伴奏分開來, 只需要打開終端機輸入幾道指令即可, 真佩服發明出這些 軟體 的高手, 造福人群啊!

想把聖歌隊影音裡的人聲和鋼琴伴奏分離,只要一份腳本就搞定!

開源軟體 Demucs

Demucs軟體是音源分離工具,主要用來將音樂分離為不同的音軌,例如人聲、鼓、貝斯、其他樂器。使用之前必須要先安裝一些相依的軟體,例如yt-dlp、ffmpeg、python3,AI都可以 幫忙處理 這些軟體,只要我們把需求列清楚,就可以請AI完成一鍵安裝、執行人聲和伴奏分離的腳本。

把以下腳本存檔並命名為split.sh。每道指令前附有說明:

#!/bin/bash
set -e

echo "🔧 更新並安裝必要系統套件..."
sudo apt-get update
sudo apt-get install -y yt-dlp ffmpeg python3-venv python3-pip build-essential python3-dev

WORKDIR=~/Music/MusicSplit
VENV_DIR=~/my-python-env

echo ""
echo "📁 建立資料夾:$WORKDIR"
mkdir -p "$WORKDIR"

if [ ! -d "$VENV_DIR" ]; then
    echo ""
    echo "🐍 建立 Python 虛擬環境:$VENV_DIR"
    python3 -m venv "$VENV_DIR"
fi

echo ""
echo "⚡ 啟用虛擬環境"
source "$VENV_DIR/bin/activate"

echo ""
echo "⬆️ 升級 pip,並安裝 Demucs(會顯示安裝進度)"
pip install --upgrade pip
pip install demucs

echo ""
echo "🎵 請輸入 YouTube 影片網址 (例如:https://youtu.be/iMZHVGMgxJU):"
read -p "👉 網址: " YT_URL

cd "$WORKDIR"

echo ""
echo "⬇️ 正在下載音訊..."
yt-dlp -f bestaudio --extract-audio --audio-format mp3 "$YT_URL"

MP3_FILE=$(ls -t *.mp3 | head -n 1)

echo ""
echo "🎧 音樂下載完成:$MP3_FILE"

OUTPUT_FOLDER="${WORKDIR}/demucs_output_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$OUTPUT_FOLDER"

echo ""
echo "🧪 使用 Demucs 分離人聲與伴奏..."
demucs --two-stems=vocals -o "$OUTPUT_FOLDER" "$MP3_FILE"

echo ""
echo "✅ 分離完成!"
echo "原始檔案:$WORKDIR/$MP3_FILE"
echo "分離檔案在:$OUTPUT_FOLDER/$MP3_FILE/ (會有 vocals 和 other 兩個 wav 檔)"
echo ""
echo "🎉 所有檔案皆存於資料夾:$WORKDIR"

在目錄/資料夾執行腳本

預設在~/底下執行,我則是把檔案搬到Music目錄去執行,也把音檔都放在新增的MusicSplit目錄,方便我尋找檔案。

輸入音源

執行的中途,終端機會要求輸入Youtube的網址好下載影音,以便後續處理。

使用方式如下:

  1. 將腳本存成 ~/Music/split.sh
  2. 給可執行權限:
chmod +x ~/Music/split.sh
  1. 執行:
~/Music/split.sh

或直接在目錄中執行

./split.sh

日後執行的腳本

已安裝相關的軟體了,日後用以下名為run_split.sh的腳本分離人聲和伴奏,可選擇輸入音源是網址或本機音檔:

(我的電腦必須在虛擬環境下執行python完成任務,您可以選擇刪除跟虛擬環境有關的指令。)

#!/usr/bin/env bash
# ----------------------------------------------------
# 互動式 Demucs 分離器(只問兩題)- URL 會先用 yt-dlp 下載 -> 再分離
# - 問題1:2軌(人聲/伴奏) 或 4軌(人聲/鼓/貝斯/其它)
# - 問題2:輸入來源(單檔/資料夾/URL,可直接拖曳到終端機)
# 預設:
#   venv=/home/bengju/demucs_env/venv
#   out=~/Music/demucs_out
#   model=htdemucs, jobs=1, 僅用GPU(-d cuda)
#   URL 下載暫存:~/Music/demucs_cache
# 特色:
#   - 不產生日誌檔(包含每檔 .log 與 url_download.log)
#   - 支援拖曳路徑(自動去除引號/尾端空白、展開 ~、還原跳脫空白)
#   - 橫向進度條(#條 + mm:ss)
# ----------------------------------------------------

# 只允許在 bash 下執行,避免 sh/dash 造成 set/陣列/[[ ]] 相容性問題
[ -n "$BASH_VERSION" ] || { echo "請用 bash 執行:chmod +x run_split.sh && ./run_split.sh"; exit 1; }
set -Eeuo pipefail

# === 可調預設值 ===
DEFAULT_VENV="/home/bengju/demucs_env/venv"
DEFAULT_OUT="$HOME/Music/demucs_out"
DEFAULT_MODEL="htdemucs"
DEFAULT_JOBS=1
CACHE="$HOME/Music/demucs_cache"

# === 輸出樣式與輔助 ===
BOLD="$(tput bold 2>/dev/null || true)"; RESET="$(tput sgr0 2>/dev/null || true)"
log(){ printf "%s\n" "$*"; }
err(){ printf "❌ %s\n" "$*" >&2; }
ok(){  printf "✅ %s\n" "$*"; }

# === 橫向進度條(不定長;右側 mm:ss),不產生日誌 ===
progress_bar() {
  local pid=$1 msg="$2" width=30
  local start_ts=$(date +%s) now elapsed pos=0 dir=1 step=1
  printf "%s\n" "$msg"
  while kill -0 "$pid" 2>/dev/null; do
    now=$(date +%s); elapsed=$(( now - start_ts ))
    pos=$(( pos + dir*step ))
    (( pos >= width )) && { dir=-1; pos=$((width-1)); }
    (( pos < 0 ))      && { dir=1;  pos=0; }
    printf "\r[%s%s]  (%02d:%02d)" \
      "$(printf '%*s' $((pos+1)) '' | tr ' ' '#')" \
      "$(printf '%*s' $((width-pos-1)) '')" \
      $((elapsed/60)) $((elapsed%60))
    sleep 0.2
  done
  now=$(date +%s); elapsed=$(( now - start_ts ))
  printf "\r[%s]  (%02d:%02d)\n" "$(printf '%*s' $width '' | tr ' ' '#')" \
    $((elapsed/60)) $((elapsed%60))
}

# === 路徑清理:處理拖曳路徑的引號/尾端空白/跳脫空白/家目錄波浪 ===
sanitize_input() {
  local s="$1"
  # 去前後空白
  s="$(printf '%s' "$s" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')"
  # 移除最外層的單/雙引號
  if [[ "$s" =~ ^\'(.*)\'$ ]]; then
    s="${BASH_REMATCH[1]}"
  elif [[ "$s" =~ ^\"(.*)\"$ ]]; then
    s="${BASH_REMATCH[1]}"
  fi
  # 將被跳脫的空白還原(e.g., path\ with\ space -> path with space)
  s="${s//\\ / }"
  # 展開家目錄波浪(若以 ~ 開頭)
  if [[ "$s" == "~"* ]]; then
    s="${s/#\~/$HOME}"
  fi
  printf '%s' "$s"
}

# === 互動:兩題 ===
read -r -p "選擇模式 (2=人聲+伴奏;4=人聲+鼓+貝斯+其它) [4]: " MODE
MODE="${MODE:-4}"
while [[ "$MODE" != "2" && "$MODE" != "4" ]]; do
  echo "輸入無效,請輸入 2 或 4"
  read -r -p "選擇模式 (2 或 4): " MODE
done

read -r -p "輸入來源(檔案/資料夾/URL,可直接拖曳): " INPUT
while [[ -z "$INPUT" ]]; do
  echo "輸入來源不可空白"
  read -r -p "輸入來源(檔案/資料夾/URL,可直接拖曳): " INPUT
done

# 針對拖曳過來的路徑/手動輸入做清理
INPUT="$(sanitize_input "$INPUT")"

# === 環境檢查 ===
[[ -d "$DEFAULT_VENV" ]] || { err "找不到虛擬環境:$DEFAULT_VENV"; exit 1; }
# shellcheck disable=SC1091
source "$DEFAULT_VENV/bin/activate" || { err "啟用 venv 失敗"; exit 1; }
command -v demucs      >/dev/null 2>&1 || { err "venv 內找不到 demucs"; exit 1; }
command -v nvidia-smi  >/dev/null 2>&1 || { err "找不到 nvidia-smi(請先安裝驅動)"; exit 1; }
mkdir -p "$DEFAULT_OUT" "$CACHE"

# === 蒐集輸入檔(含 URL 下載)— 全程不寫日誌檔 ===
declare -a files
if [[ "$INPUT" =~ ^https?:// ]]; then
  # 需要 yt-dlp + ffmpeg
  command -v yt-dlp >/dev/null 2>&1 || { err "URL 模式需要 yt-dlp;請先安裝:sudo apt install yt-dlp 或 pipx install yt-dlp"; exit 1; }
  command -v ffmpeg >/dev/null 2>&1 || { err "URL 模式需要 ffmpeg;請先安裝:sudo apt install ffmpeg"; exit 1; }

  # 下載為 wav(最佳音質),檔名「標題-ID.wav」;不寫日誌檔
  outtpl="%(title)s-%(id)s.%(ext)s"
  ytcmd=(yt-dlp --no-playlist -f "bestaudio/best" --extract-audio --audio-format wav --audio-quality 0
         -o "$CACHE/$outtpl" "$INPUT")

  ( "${ytcmd[@]}" >/dev/null 2>&1 ) &   # 若想看 yt-dlp 輸出,移除 >/dev/null 2>&1
  pid=$!
  progress_bar "$pid" "下載音檔中(yt-dlp)"
  if ! wait "$pid"; then
    err "下載失敗(未記錄日誌,請重試或移除 >/dev/null 2>&1 以顯示詳情)"; exit 1
  fi

  # 找到最新的 wav 當作下載結果
  mapfile -t candidates < <(find "$CACHE" -maxdepth 1 -type f -iname "*.wav" -printf "%T@ %p\n" | sort -nr | awk '{ $1=""; sub(/^ /,""); print }')
  [[ ${#candidates[@]} -gt 0 ]] || { err "下載後找不到 WAV 檔"; exit 1; }
  dlfile="${candidates[0]}"
  files=("$dlfile")

elif [[ -d "$INPUT" ]]; then
  while IFS= read -r -d '' f; do files+=("$f"); done < <(
    find "$INPUT" -type f \( -iname '*.wav' -o -iname '*.flac' -o -iname '*.mp3' \
                             -o -iname '*.m4a' -o -iname '*.aac'  -o -iname '*.ogg' \
                             -o -iname '*.opus' -o -iname '*.wma' \) -print0 | sort -z
  )
  [[ ${#files[@]} -gt 0 ]] || { err "資料夾裡找不到支援的音訊檔。"; exit 1; }

elif [[ -f "$INPUT" ]]; then
  files=("$INPUT")

else
  err "找不到檔案或資料夾:$INPUT"; exit 1
fi

# === Demucs 參數(只用 GPU)===
BASE_CMD=(demucs -d cuda --jobs "$DEFAULT_JOBS" -n "$DEFAULT_MODEL" -o "$DEFAULT_OUT")
stem_args=()
[[ "$MODE" == "2" ]] && stem_args=(--two-stems=vocals)

# === 分離作業(逐檔、顯示進度、不寫任何 .log)===
total=${#files[@]}
echo "${BOLD}開始分離(GPU 模式)${RESET}"
echo "模式:$MODE 軌"
echo "輸入:$INPUT"
echo "發現檔案數:$total"
echo "輸出:$DEFAULT_OUT"
echo

idx=0
for f in "${files[@]}"; do
  idx=$((idx+1))
  base="$(basename "$f")"
  echo "——— [${idx}/${total}] $base ——"

  # 不重定向到檔案 → 不會產生每檔 .log
  ( "${BASE_CMD[@]}" "${stem_args[@]}" "$f" >/dev/null 2>&1 ) &  # 想看 demucs 詳細輸出,移除 >/dev/null 2>&1
  pid=$!
  progress_bar "$pid" "分離中:$base"

  if ! wait "$pid"; then
    err "[${idx}/${total}] 失敗:$base(未記錄日誌;必要時請移除 >/dev/null 2>&1 以顯示命令輸出)"
  else
    ok  "[${idx}/${total}] 完成:$base"
  fi
done

echo
ok "全部處理完成!"
echo "輸出根目錄:$DEFAULT_OUT"

記得同樣給run_split.sh可執行的權限+x:

chmod +x ~/Music/run_split.sh

開源軟體很強大,終端機很簡潔有力,目前大概只有想不到,沒有做不到的事。

  • 註:因為某些軟硬體高齡的原故,我必須在虛擬的python裡執行Demucs,所以,請依各位電腦的實際情況執行。總之,多問問AI,她很了解自己和同胞。

上次修改於 2025-07-25