フェルマータ

個人用のメモ。ソフトウェアの導入とかが多くなる予定。

今さら C++ で WORLD のラッパーライブラリを書いてみた

ポエム

現実世界の不具合で出勤時間が消失した分体力が余っているのか、余暇に回す気力がだいぶ戻ってきたので 1000000000 年ぶりくらいに趣味プロをしている。とはいえ別段すごく作りたいものがあるわけではないので、惰性で昔作りたかった WORLD をいい感じに抽象化したライブラリを作ってみた。これ使って何しようかは浮かんではいないのだが、音声の波形を切り貼りするコードはぼくが遊ぶ分にはだいぶ書きやすくなるので一区切りして記事にしておく。

やったこと

WORLD を利用した C++ のラッパーを作った。その際昔 v.Connect 作ってて面倒くさかった周期性・非周期性成分の扱いを抽象化した。と言ってもなんのこっちゃなので下記のコードを見てほしい。

    void SpectralEnvelopeEstimation(double *x, int x_length, WorldParameters *world_parameters)
    {
        CheapTrickOption option;
        InitializeCheapTrickOption(world_parameters->fs, &option);

        // put default parameters into CheapTrickOption.
        option.q1 = -0.15;
        option.f0_floor = 71.0;

        // Parameters setting and memory allocation.
        world_parameters->fft_size =  GetFFTSizeForCheapTrick(world_parameters->fs, &option);
        world_parameters->spectrogram = new double *[world_parameters->f0_length];
        for (int i = 0; i < world_parameters->f0_length; ++i) {
            world_parameters->spectrogram[i] = new double[world_parameters->fft_size / 2 + 1];
        }

        DWORD elapsed_time = timeGetTime();
        CheapTrick(x, x_length, world_parameters->fs, world_parameters->time_axis, world_parameters->f0, world_parameters->f0_length, &option, world_parameters->spectrogram);
        std::cout << "CheapTrick: " << timeGetTime() - elapsed_time << " [msec]" << std::endl;
    }

このコードの大本はWORLD のサンプルコードである。元のコードは下記を参照のこと。

音声は、趣味で DX ライブラリ使ってゲームもどき作ったり仕事でウェブアプリを作るのと違いだいぶ扱いが面倒くさく(修羅場P調べ)、 v.Connect で歌声合成するときには時系列の波形データそのものでは扱いづらいため波形データから音の高さ・声の母音の成分・声のそれ以外の成分(雑に書くと正しくないのでこのあとはそれぞれ、F0、周期性成分、非周期性成分と言う)の3つの成分に WORLD を使ってばらしてから再合成していた。このばらした情報は先に挙げたコードだと例えば周期性成分は double **WorldParameters::spectrogram の中に二次元配列として格納される。この二次元配列はいわゆる音のスペクトルの画像なので、サンプルのコードのように小さいプログラムなら別にこの持ち方でも十分便利ではあり、 v.Connect も結局実装的には本当に楽なので内部的に波形を二次元配列に直しては切り貼りをする処理が書かれている。

しかしこのデータの持ち方、小規模のうちは特に気にならないのだが UTAU 音源を読み込んだりして波形の数が二桁以上になってくると二次元配列だけにバカスカメモリを食い始めて扱いが面倒になってくる。さらによく考えてみると二次元配列で持つかどうかは別段実装都合であって、翻って v.Connect みたいに UTAU 音源使って声を切り貼りするみたいな用途に注目すれば、単純にある時刻の F0、周期性成分、非周期性成分がわかりさえすればいいことに気づく。となると下記のインタフェースだけ実装されていれば音声の切り貼りをする歌声合成器を作るのには十分なのだ。

class Spectrogram {
public:
    /**
     * Spectrogram#pickUpSpectrumAt picks up an instant spectrum on the specified moment.
     */
    virtual bool pickUpSpectrumAt(Spectrum *destination, double ms) const = 0;

    /**
     * Spectrogram#f0At returns f0 value of spectrogram at ms[milli seconds].
     */
    virtual double f0At(double ms) const = 0;
};

裏側に二次元配列を持つかどうかは実装詳細の都合なので何でも良い。今回はバカスカメモリを食うのは嫌だったので、ある時刻の周期性成分と非周期性成分はスペクトルを引っ張ってくる pickUpSpectrumAt メソッド内で計算する実装に挑戦した。数年前のぼくがどうしても実力不足で作れなかったのだが数年ぶりに書いてみたらなんとかなったので大変うれしい。

できたものと使い方

github.com

実装者の環境は下記である。この書き方であってんのか? C++ の事情はよくわからんのでぼくの環境でしか動かないかもしれないが調べる余力はない。

harun@MyComputer MINGW64 /g/Projects/uzume/uzume_dsp
$ cmake --version
cmake version 3.14.4

CMake suite maintained and supported by Kitware (kitware.com/cmake).

harun@MyComputer MINGW64 /g/Projects/uzume/uzume_dsp
$ g++ --version
g++.exe (Rev2, Built by MSYS2 project) 8.3.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

とりあえず波形の分析と合成の部分を自作のインタフェースで行えるところまで実装した。オリジナルの WORLD と音質は差がない(個人の感想です)になったので一旦満足。裏側の実装はどうでもいいと言ったが、波形を与えれば二次元配列を裏で確保することなく、その波形の特定の時刻の F0、周期性成分、非周期性成分を計算できるところまで作った。 使い方は CMakeLists.txt に下記のような依存を書く。この書き方が洗練されているかは C++ は普段遣いではないのでよく知らないので参考程度に。

## Dependencies
ExternalProject_add(uzume_dsp
        PREFIX ${CMAKE_CURRENT_BINARY_DIR}/uzume_dsp
        GIT_REPOSITORY https://github.com/haruneko/uzume_dsp
        GIT_TAG master
        INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}
        CMAKE_ARGS "-DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR}"
        )
add_dependencies(your_app uzume_dsp)
link_directories(your_app PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/lib)
target_include_directories(your_app PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/include)
target_link_libraries(your_app PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/lib/libuzume_dsp.a)

んで、例えば /path/to/input.wav/path/to/output.wav に分析・再合成するのであれば下記のようなコードを書けばよい。

#include <algorithm>

#include "SynthesizeImpulseResponseWithWORLD.hpp"
#include "SynthesizePhraseWithWORLD.hpp"
#include "Waveform.hpp"
#include "WaveformSpectrogram.hpp"

using namespace uzume::dsp;

// This is a sample to use dsp directory.
int main() {
    const char *inputPath = "/path/to/input.wav";
    const char *outputPath = "/path/to/output.wav";
    Waveform *input = Waveform::read(inputPath);
    Waveform *output = new Waveform(input->length, input->samplingFrequency);
    WaveformSpectrogram spectrogram(input);

    SynthesizeImpulseResponseWithWORLD irs(spectrogram.fftSize(), input->samplingFrequency);
    SynthesizePhraseWithWORLD synthesize(&irs);

    for(unsigned int i = 0; i < output->length; i++) {
        output->data[i] = 0.0;
    }

    PhraseSignal s(output->data, /* indexMin = */ 0, /* indexMax = */ output->length, output->samplingFrequency);
    PhraseParameters p(&spectrogram, /* startPhase = */ 0.0, /* startFractionalTimeShift = */ 0.0);

    synthesize(&s, &p);

    output->save(outputPath);
}

肝は Spectrogram クラスで、このクラスをとりあえず実装して spectrogram に突っ込んでおけば上記のようなコードで音声が合成できる。上のコードでは WaveformSpectrogram という波形データをスペクトルとして扱えるようにするクラスを利用しているが、他の実装に変えて例えば大量の波形データの切り貼りしたスペクトルを返すような実装にしてあげれば、いい感じの切り貼りツールが作れる。