Wizard Notes

Python, JavaScript を使った音楽信号分析の技術録、作曲活動に関する雑記

Pythonによる音響音楽信号処理:クロスフェード自動生成 (1)アルゴリズムの概要

※2018/10/19: Bloggerから移行しました

概要

Python(LibROSA)を用いた音響音楽信号処理として、クロスフェード自動生成アルゴリズムを設計します。
具体的には、複数の曲をフォルダに入れておけば、クロスフェード音源を自動で作ってくれるアルゴリズムを作りたいと思います。

LibROSAについて

音響音楽信号処理用のパッケージです。
メル周波数解析やMFCC、定Q変換によるクロマベクトル生成といった周波数領域の解析のほか、BPM・ビート検知、様々な楽曲特徴量を算出する関数が用意されています。

GitHub

Document

問題設定

問題設定として、すでに曲順が決まっている複数の楽曲が用意されているというタスクにします。また、曲順はファイル名で指定することとします。

例: 01.wav, 02.wav, ..., 09.wav

アルゴリズム設計と実装

必要な処理の列挙

  1. 各曲に対して、クロスフェード区間を切り出す
  2. 各曲のクロスフェード区間の先端・終端部に、それぞれフェードイン・フェードアウト処理を行う
  3. 各曲において、前後の楽曲とフェードイン・フェードアウト区間を繋げて、1つの音源として出力する
上記の処理を、以下のような関数で実現することとします。

  1. get_highlight(x, duration):
    源信号xからduration秒のクロスフェード区間を切り出す
  2. add_fade(x, fadetime):
    源信号xに対して、先端・終端部にfadetime秒間のフェードイン・フェードアウト処理をそれぞれ加える。
  3. gen_xfade(x_pre, x_next, fadetime):
    2つの信号x_preとx_nextをfadetime秒間のフェードイン・フェードアウト区間で繋げて1つの音源とする。ただし、x_preがNoneの時はx_nextをそのまま返す

Pythonスクリプトの全体像

import sys, os
import numpy as np
import librosa
sr = 44100
dirpath  = sys.argv[1] 
duration = int(sys.argv[2]
fadetime = int(sys.argv[3]) 
files = os.listdir(dirpath)
files.sort()
xfade = None
for file in files:
    _, ext = os.path.splitext(file)
    if ext.lower() in ['.wav', '.mp3', '.flac']:
        y, sr = librosa.load(os.path.join(dirpath,file),\
                             sr=sr, mono=False)
        y = get_highlight(y, duration, sr)
        y = add_fade(y, fadetime, sr)
        xfade = gen_xfade(xfade, y, fadetime, sr)
librosa.output.write_wav('_xfade.wav', xfade, sr)

ここで、srはサンプリング周波数を示し、各関数内で指定した秒数をサンプル数に変換するのに利用します。

注意点として、librosa.loadは引数monoがデフォルトではTrueになっています。この時、読みこんだ音源はモノラルになってしまうため、ステレオチャネルの音源を扱うときは必ずmono=Falseにしてください。
なお、ステレオチャネル信号をロードすると、Numpyのshapeとしては(チャンネル数、サンプル数) となります。

実行例: $> python autoxfade.py audio 10 3

audioディレクトリ以下にあるオーディオファイルを、クロスフェード区間10秒、フェードイン・フェードアウト区間それぞれ5秒としてクロスフェード音源を自動生成します。

get_highlight()

今回は、クロスフェード区間はランダムに決定します。

後日、音響特徴量や楽曲構造(サビ検出)を利用した区間抽出を解説する予定です。

def get_highlight(x, duration, sr):
    x = x.T
    length = x.shape[0]
    d_len = int(duration*sr)
    offset = np.random.randint(d_len,length-d_len)
    x_highlight = x[offset:offset+d_len].T
    return x_highlight

add_fade()

よく利用されているcosineを利用したパワー領域でのフェードイン・フェードアウトを行っています。
詳しくは、次回のクロスフェード関数デザインで解説します。

def add_fade(x, fadetime, sr):
    ft_len = int(fadetime*sr)
    r = np.arange(0, ft_len)*np.pi/ft_len
    w_fo = (0.5+0.5*np.cos(r))**0.5
    w_fi = (0.5-0.5*np.cos(r))**0.5
    
    if x.ndim == 1:
        x[0:ft_len]        *= w_fi
        x[length-ft_len::] *= w_fo
    else:
        ch, length = x.shape
        for c in np.arange(0, ch):
            x[c,0:ft_len]        *= w_fi
            x[c,length-ft_len::] *= w_fo
    return x

gen_xfade()

愚直に書いています。

def gen_xfade(x_pre, x_next, fadetime, sr):
    ft_len = int(fadetime*sr)
    if x_pre is None:
        xfade = x_next
    else:
        if x_next.ndim == 1:
            x_pre_len = x_pre.shape
            x_next_len = x_next.shape
            x_pre_len -= ft_len
            x_next_len -= ft_len
            xfade = np.c_[x_pre, np.zeros(x_next_len)]\
                  + np.c_[np.zeros(x_pre_len),x_next]
        else:
            ch, x_pre_len = x_pre.shape
            ch, x_next_len = x_next.shape
            x_pre_len -= ft_len
            x_next_len -= ft_len
            xfade = np.c_[x_pre, np.zeros((ch, x_next_len))]\
                  + np.c_[np.zeros((ch, x_pre_len)),x_next]
    return xfade

結果

自身のアルバムより5曲を用いて自動生成しました。


クロスフェード区間はランダムであるため、いい感じで繋がるかどうかは運任せです。
#これはこれで偶然性に基づくおもしろいです。

まとめ

クロスフェード自動生成アルゴリズムを設計しました。
また、クロスフェード区間をランダム選択で選ぶという試験的な手法で、設計に基づくPythonスクリプトを作成しました。

次回は、クロスフェードの窓関数設計について解説します。

次次回は、よりよいクロスフェード区間抽出のためのアルゴリズムを解説・実装します。

補足

今回のアルゴリズム設計では、曲順はユーザーが指定するものとしました。
しかし、曲順に関しての最適化も考えられます。例えば、似た曲調・音楽ジャンルで曲順をソートする、つまりいい感じに曲順を自動ソーティングしてくれるようなアルゴリズム設計も考えられます。
その場合はクロスフェード区間も関係するため少し問題が複雑になりますが、よりDJライクな、いい感じのクロスフェードを実現することができると思います。

一方で、曲順くらいユーザが決めるパラメタとして残しておくべきだという考えもでき、実装的にも比較的楽なため今回はこちらの思想で設計を行いました。