🎃

pyworldなどを用いたハモリ生成

2025/02/07に公開

はじめに

今回の記事では、ハモリ生成の手順を使うライブラリや具体的なコードを交えて説明していきます。
まず、ハモリ生成を行うための事前準備において、用意した音源(wavファイル)の主旋律と伴奏を分離することが必要です。また、この手法を用いる際に元の音源にハモリが含まれていた場合、音源分離の段階でうまく処理できないためその箇所のハモリ生成はうまくいかないことが多いです。
ハモリ生成までの手順は下記の5つに分けられます。

  1. 用意した音源を主旋律と伴奏に分離する。
  2. 主旋律の基本周波数を推定する。
  3. 伴奏に対してクロマグラムを作成する。
  4. ハモリの候補音を絞り込む
  5. ハモリを生成する。

音源分離を試してみよう

はじめにで言ったように、ハモリ生成のための前準備として音源分離を用いて、音源の主旋律と伴奏をあらかじめ用意する必要があります。
ここでは、spleeterというライブラリを使用します。

!pip install spleeter

spleeterを使用するには、上記のコマンドを用いて、spleeterをインストールする必要があります。

from spleeter.separator import Separator

input_file = 'sound.wav'  # 入力ファイルのパス

# ボーカル抽出
def extract_vocals(input_file, output_file):
    separator = Separator('spleeter:2stems')
    separator.separate_to_file(input_file, output_file, codec='wav')

vocal_file = 'vocal.wav'
extract_vocals(input_file, vocal_file)

上記のコードを試すと、主旋律と伴奏の2つに分離することが出来ます。

基本周波数を推定しよう

音源分離した主旋律を入力として、主旋律の基本周波数を推定します。この時に求めた基本周波数は後述の工程で使用します。
主旋律を求める目的としては、ハモリの生成にピッチシフトを用いているため、元となるピッチを求める必要があります。

伴奏に対して、クロマグラムを作ろう

音源分離した伴奏を入力としてクロマグラム(下図)を作成します。
クロマグラムとは、12音階の各音名の振幅を表す12次元ベクトル(クロマベクトル)を時間軸上に並べたものです。
クロマグラムを作成する目的としては、クロマグラムの

ハモリの候補音を絞り込もう

ハモリの候補音(こちらで任意に決めた)ものの中から先ほど求めた基本周波数とクロマグラムを元にハモリの候補音の中で最も適したものを選択しよう
今回のハモリの候補音は以下の通りです。

  1. 短三度上下の音
  2. 長三度上下の音
  3. 完全四度上下の音

ハモリを生成しよう

最後に上記で絞り込んだハモリの候補音の音を元にpyworldを用いてハモリ音源を生成します。
上記の内容をまとめたコードは以下の通りです。

import librosa
import numpy as np
import pyworld as pw
import soundfile as sf
from scipy.ndimage import uniform_filter1d

# ボーカルトラックと伴奏トラックを読み込み、処理する関数
def load_and_process_tracks(vocal_path, accompaniment_path, sr=48000):
    try:
        y_vocal, _ = librosa.load(vocal_path, sr=sr, dtype='float64')
        y_accompaniment, _ = librosa.load(accompaniment_path, sr=sr, dtype='float64')
    except Exception as e:
        print(f"Error loading tracks: {e}")
        return None, None

    # トラックの長さをチェック
    print(f"Vocal track length: {len(y_vocal)}, Accompaniment track length: {len(y_accompaniment)}")

    chroma = librosa.feature.chroma_stft(y=y_accompaniment, sr=sr, hop_length=int(sr*0.005))
    return y_vocal, chroma


# pyworldを使ってピッチを抽出する関数
def extract_pitch(y, sr):
    try:
        f0, t = pw.dio(y, sr)
        f0 = pw.stonemask(y, f0, t, sr)
    except Exception as e:
        print(f"Error in pitch extraction: {e}")
        return None, None

    # ピッチ抽出結果の確認
    print(f"Extracted {len(f0)} pitch values.")
    return f0, t


# クロマグラムを平滑化する関数
def smooth_chromagram(chromagram, window_size=200):
    smoothed = uniform_filter1d(chromagram, size=window_size, axis=1)
    # 平滑化前後の変化を表示
    print(f"Chromagram smoothing: original shape {chromagram.shape}, smoothed shape {smoothed.shape}")
    return smoothed

# 状態遷移確率行列と出力確率を定義
transition_probabilities_same_pitch = {
    "major_third": {"major_third": 0.7, "minor_third":0.15, "perfect_fourth": 0.1, "none": 0.05},
    "minor_third": {"major_third": 0.15, "minor_third": 0.7, "perfect_fourth": 0.1, "none": 0.05},
    "perfect_fourth": {"major_third": 0.15, "minor_third": 0.15, "perfect_fourth": 0.6, "none": 0.1},
    "none": {"major_third": 0.4, "minor_third": 0.4, "perfect_fourth": 0.15, "none": 0.05}
}

transition_probabilities_different_pitch = {
    "major_third": {"major_third": 0.35, "minor_third": 0.35, "perfect_fourth": 0.25, "none": 0.05},
    "minor_third": {"major_third": 0.35, "minor_third": 0.35, "perfect_fourth": 0.25, "none": 0.05},
    "perfect_fourth": {"major_third": 0.35, "minor_third": 0.35, "perfect_fourth": 0.25, "none": 0.05},
    "none": {"major_third": 0.3, "minor_third": 0.3, "perfect_fourth": 0.3, "none": 0.1}
}

output_probabilities = {
    "major_third": None,
    "minor_third": None,
    "perfect_fourth": None,
    "none": None
}

# クロマグラムから出力確率を計算する関数
def calculate_output_probabilities(chroma_frame):
    total = np.sum(chroma_frame)
    if total == 0:
        return chroma_frame
    return chroma_frame / total

# ハモリ音を選択する関数
def choose_harmony_note_viterbi(original_pitch, harmony_candidates, chroma_frame, prev_state, transition_probabilities):
    if original_pitch == 0:
        return 0, "none"

    # クロマフレームから出力確率を計算
    chroma_prob = calculate_output_probabilities(chroma_frame)

    max_prob = 0
    selected_harmony_pitch = None
    selected_state = None

    for state in harmony_candidates:
        if state == "none":
            candidate_pitch = 0
        elif state == "major_third":
            candidate_pitch = original_pitch * (2 ** (-4/12))
        elif state == "minor_third":
            candidate_pitch = original_pitch * (2 ** (-3/12))
        elif state == "perfect_fourth":
            candidate_pitch = original_pitch * (2 ** (-5/12))

        if candidate_pitch <= 0:
            continue

        # 遷移確率と出力確率を組み合わせて最大確率の状態を選択
        if prev_state in transition_probabilities:
            prob = transition_probabilities[prev_state][state] * chroma_prob[int(np.round(np.log2(candidate_pitch / 440) * 12) + 9) % 12]
            if prob > max_prob:
                max_prob = prob
                selected_harmony_pitch = candidate_pitch
                selected_state = state
        else:
            selected_harmony_pitch = candidate_pitch
            selected_state = state

    if selected_harmony_pitch is None:
        return 0, "none"

    return selected_harmony_pitch, selected_state

# ハーモニーを生成する関数
def generate_harmony(y_vocal, sr, chromagram, output_file, harmony_direction='down'):
    f0, t = extract_pitch(y_vocal, sr)
    sp = pw.cheaptrick(y_vocal, f0, t, sr)
    ap = pw.d4c(y_vocal, f0, t, sr)
    smoothed_chromagram = smooth_chromagram(chromagram)

    harmony_pitch = np.copy(f0)
    harmony_choices = []

    prev_state = "none"

    for i in range(len(f0)):
        if f0[i] == 0:
            harmony_pitch[i] = 0
            harmony_choices.append("none")
            continue

        harmony_candidates = ["major_third", "minor_third", "perfect_fourth", "none"]

        # ピッチが前の音と同じかどうかを比を用いて判断
        if i > 0 and f0[i-1] != 0 and (f0[i] / f0[i-1] > 0.99 and f0[i] / f0[i-1] < 1.01):
            selected_note, prev_state = choose_harmony_note_viterbi(f0[i], harmony_candidates, smoothed_chromagram[:, i], prev_state, transition_probabilities_same_pitch)
        else:
            selected_note, prev_state = choose_harmony_note_viterbi(f0[i], harmony_candidates, smoothed_chromagram[:, i], prev_state, transition_probabilities_different_pitch)

        harmony_choices.append(prev_state)
        harmony_pitch[i] = selected_note

    harmonic = pw.synthesize(harmony_pitch, sp, ap, sr)
    sf.write(output_file, harmonic, sr)

    return harmony_choices

# メイン処理
vocal_wav_path = '/content/vocal.wav/sound/vocals.wav'
accompaniment_wav_path = '/content/vocal.wav/sound/accompaniment.wav'

y_vocal, chromagram_accompaniment = load_and_process_tracks(vocal_wav_path, accompaniment_wav_path)
harmony_choices = generate_harmony(y_vocal, 48000, chromagram_accompaniment, 'harmony_sound.wav', harmony_direction='down')

最後に

上記を試して好きな曲のハモリパートを生成してみてください!!

Discussion