音楽プログラミングの超入門(仮)

Python / 音楽情報処理 初心者が、初心者にも分かるような記事を書きたい。

(2)音のタイムストレッチとピッチシフト:【波形領域】

Pitch shift

音のタイムストレッチとピッチシフト

別々にやりたい

前回は、再生速度とピッチが同時に変わるリサンプリングを紹介しました。しかし、今の時代、タイムストレッチとピッチシフトを別々にできないとあまり使い物になりません。そこで、今回以降はそれを可能とする技術を紹介していきます。

波形領域での処理

今回扱うのは、波形領域(時間領域)での処理です。つまり、入力された音響信号をそのままイジって、タイムストレッチやピッチシフトに挑戦します。

波形領域での処理に関しては、超絶分かりやすいページを発見したので、参考にしてください(下のリンク)。

タイムストレッチ、ピッチシフトのアルゴリズム
タイムストレッチ、ピッチシフトのアルゴリズム
タイムストレッチ、ピッチシフトのアルゴリズム

大事なリンクなので3回貼りました。
正直、このページみたら大体分かるし、記事書く必要ないんじゃないか・・と思ったりしましたが、音がどんな感じになるか聞いてみたい人もいるんじゃないかと思ったので、そこらへん中心に書いていきます。

以下の記事は上記サイトに沿って書いてあるので、並列して読んでもらえると分かりやすいと思います。(図とか作るの面倒くさいから上のサイト見てください・・・。)

タイムストレッチとピッチシフトを別々に行えると書きましたが、基本的な流れとしては、まずタイムストレッチをして、次にその信号をリサンプリングすることで、任意の再生速度、ピッチを実現します。

一般的に、再生速度をv倍、ピッチをp倍したいときは、 タイムストレッチのアルゴリズムで再生速度をv/p倍してから、 波形を単純に1/p倍に拡大、縮小します。

タイムストレッチの基本的アルゴリズム

見出し名も上記サイト、丸パクリです。

タイムストレッチの最も愚直な方法は、信号を適当な幅で切り出して繋げる、切り出し幅の再生速度倍だけシフトして同様に切り出して繋げる、という風に無理やりな感じの手法です。(文章で書くと分かりにくい・・・。)

これでは、繋げるところでプツプツ音が発生してしまうのは何となく予想できますね。

クロスフェード

そこで、もう少し滑らかに繋げようという試みが、クロスフェードです。つまり、繋ぎ目のところで、前の信号を段々弱く(フェードアウト)、後ろの信号を段々強く(フェードイン)していくことで、繋ぎ目が曖昧な感じになります。

繋ぎ目の位相がちょうど同じならいいですが、少しでもずれていれば信号が打ち消し合うところがでてきて、その部分のパワーが弱くなります。

なんだか、モワモワした音が出来上がりそうですね。

そこでサインカーブなどを用いてクロスフェードを行うと、少しこれがマシになるようです。今回の実装ではハミング窓を使ってみました。多分あまり変わらないでしょう。

音を聴いてみよう

実装は最後にまとめて載せますが、とりあえずこの方法でタイムストレッチした音を聴いてみましょう。サンプル音声は前回と同じく「あらゆる現実~」です。

クロスフェード無し”

再生速度は 0。5 倍、数値は信号の切出し幅です。

オリジナル

30msec

50msec

70msec

ブロックのサイズをある程度大きくしないと、低周波が再現できません。 しかし、大きくしすぎるとスムーズに聞こえません。50msec程度がよいと思います。

と上記サイトに書いてありますが、確かにそんな感じがします。少なくとも 70 msec はかなり違和感があります。

クロスフェード有り”

切出し幅 50 msec とし、フェードする(重ねる)幅を変えて実験してみました。

(フェード幅) 10msec

(フェード幅) 30msec

(フェード幅) 50msec

おぉ!プツプツ音が無くなった!!
フェード幅を大きくすると、残響感が強くなるようです。上の音を聴くと、フェード幅は 30 msec くらいがよさそうですね。

さらに改良する

リンク先に頼りっぱなしの本記事ですが、なんとか存在意義を見出そうと、この手法の改良を試みてみました。

先ほどの、タイムストレッチした信号とその音量軌跡を見てみると、こんな感じ。

Pitch shift

明らかに、切出し幅間隔で、音量が振動しています。これがモワモワ感の一因なのは間違いないので、これの除去にチャレンジします。

イデアは非常に単純で、こんな感じ↓。

  1. 音量軌跡にローパスフィルタかけたら平滑化される気がする
  2. じゃあ、平滑化された音量軌跡を元の音量軌跡で割ったやつを信号にかけたらモワモワが消えそう

これだけです。音量軌跡を平滑化するところは、単純移動平均でもいいかも?でもやっぱり、平滑化された軌跡が元の軌跡の中心を通るようにするにはローパスフィルタの方がいいのかもしれない。

実際にやってみた結果がこんな感じです。

Pitch shift

(;^ν^)ん?超微妙に改善してる?

とりあえず音聴こう。

音量修正前

音量修正後

微妙過ぎるだろ!!でもちょっと改善してる。

ピッチシフト

ピッチシフトはリサンプリングするだけなので、前回の記事を参考に適当に実装してください。

実装

今回の実装を載せておきます。

# -*- coding: utf-8 -*-
"""
時間領域でのピッチシフト・タイムストレッチ
"""
from scipy import arange, array, around, ceil, fft, hamming, ifft, interp, linspace, sqrt, zeros

from scipy.io.wavfile import read, write
from matplotlib import pylab as pl

from stft import stft

# ========
#  HELPER
# ========
def triangle(length):
    h = int(ceil(length / 2.))
    ret = zeros(length, dtype = float)
    ret[: h] = (arange(h, dtype = float) / (h - 1))
    if length % 2 == 0:
        ret[- h :] = ret[h - 1 :: -1]
    else:
        ret[- h + 1 :] = ret[h - 1 :: -1]
    return ret

### 信号のダイナミクス(音量)を計算
def calc_dynamics(sig, fs, cut_wid = 0.01):
    sig = array(sig, dtype = "float16") / max(sig)
    cut_num = int(round(cut_wid * fs))
    shift_num = cut_num / 4
    
    N = (len(sig) - cut_num) / shift_num + 1
    ret = zeros(N, dtype = float)
    for n in xrange(N):
        s = n * shift_num
        e = s + cut_num
        ret[n] = sqrt((sig[s : e] ** 2).sum()  / float(cut_num))
    return ret
   
# =============================
#  Time Stretch in time domein
# =============================
"""
cut_wid : Width of cutting
cross_wid : Width of crossfade
"""
def timeStretch(sig, fs, scale, cut_wid = 0.06, cross_wid = 0.03, fade_func = None):
    ### sec -> time frame
    cut_num = int(round(cut_wid * fs))
    cross_num = int(round(cross_wid * fs))
    shift_num = int(round(cut_num / float(scale)))

    # =================
    #  タイムストレッチ
    # =================

    ### フェード関数
    if fade_func == None:
        win = triangle(cross_num * 2)
    else:
        win = fade_func(cross_num * 2)

    L = len(sig)
    I = (L - (cut_num + cross_num)) / shift_num + 1
    
    new_sig = zeros(cut_num * (I + 1), dtype = float)
    for i in xrange(I):
        s = i * shift_num
        e = s + cut_num + cross_num
        tmp_sig = sig[s : e].copy()
        
        ### クロスフェード
        if cross_num != 0:
            tmp_sig[-cross_num :] *= win[- cross_num :]
            if i != 0:
                tmp_sig[: cross_num] *= win[: cross_num]

        new_s = i * cut_num
        new_e = new_s + len(tmp_sig)
        new_sig[new_s : new_e] += tmp_sig

    new_sig = array(new_sig)

    # ==============
    #  音量を平滑化
    # ==============

    ### 音量を計算
    dynamics = calc_dynamics(new_sig, fs)
    dynamics = interp(linspace(0, len(dynamics), num = len(new_sig)), arange(len(dynamics)), dynamics)
   
    ### 音量に含まれるノイズの周波数
    noise_hz = 1. / cut_wid
    noise_ind = noise_hz * len(dynamics) / (fs / 2.)

    ### 音量軌跡にローパスフィルタ
    lowpass_hz = 7.
    lowpass_ind = lowpass_hz * len(dynamics) / (fs / 2.)
    fft_dyn = fft(dynamics)
    fft_dyn[lowpass_ind : - lowpass_ind] = 0
    new_dynamics = ifft(fft_dyn).real
    new_dynamics[new_dynamics < 0] = 0

    ### 信号の音量を修正
    scale = zeros(len(new_sig), dtype = float)
    scale[dynamics < max(dynamics) / 30.] = 1.
    scale[new_dynamics == 0] = 1.
    scale[scale == 0] = new_dynamics[scale == 0] / dynamics[scale == 0]
    scaled_new_sig = new_sig * scale
    scaled_new_sig *= max(abs(sig)) / max(abs(scaled_new_sig))

    # ======
    #  描画
    # ======
    x_up = 40000
    fig = pl.figure()
    fig.patch.set_alpha(0.0)
    fig.add_subplot(311)
    pl.title("Time-stretched signal")
    pl.plot(new_sig)
    pl.xlim([0, x_up])
    pl.ylim([-25000, 25000])
    fig.add_subplot(312)
    pl.title("Volume dynamics")
    pl.plot(dynamics, label = "raw")
    pl.plot(new_dynamics, label = "low-pass", linewidth = 2)
    pl.legend(loc='upper right')
    pl.xlim([0, x_up])
    fig.add_subplot(313)
    pl.title("Modified time-stretched signal")
    pl.plot(scaled_new_sig)
    pl.xlim([0, x_up])
    pl.ylim([-25000, 25000])
    
    pl.tight_layout()
    pl.show()

    return scaled_new_sig
    

def test(wav_file):
    fs, data = read(wav_file)
    new_data = timeStretch(data, fs, 2, cut_wid = 0.05, cross_wid = 0.03, fade_func = hamming)

    new_data = array(around(new_data), dtype = "int16")
    write("../wav/output/a01_timestretch.wav", fs, new_data)

if __name__ == "__main__":
    input_wav = "../wav/a01.wav"
    test(input_wav)

関連記事