フェルマータ

個人用のメモ。ソフトウェアの導入とかが多くなる予定。ライセンスの気になる方はこのブログに載せたコードは修正 BSD に準ずるものと考えてください。

今さら 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 という波形データをスペクトルとして扱えるようにするクラスを利用しているが、他の実装に変えて例えば大量の波形データの切り貼りしたスペクトルを返すような実装にしてあげれば、いい感じの切り貼りツールが作れる。

直リンク系ライブラリの CMakeLists.txt の書き方調べた。

ポエム

CMakeLists.txt は便利なのだが、ちゃんと書き方を考えるほど切羽詰まった場面が自分にはなかった。今回は書いていて気になったのでちゃんと書く方法を調べたりしてみた。正直知見がなかったので何でググったっけを書いておくメモみたいなものである。

やりたいこと

  • static に依存させるライブラリを作成したい
  • 作成したライブラリは ExternalProject などで github 上から依存解決をしたい
  • なるべくならグローバルを汚染しないで依存解決をしたい

要は cmake でのライブラリの書き方がわかれば良い。ただ、静的ライブラリであれば add_subdirectory すればよくね?という気持ちがないではない。今回は何が何でも静的リンクさせたかった、 cmake 経由でやれたことなかったからね。

やること

ライブラリ側

特殊なことは今回はしないのでこれだけ。target_sources コマンドでコンパイル対象を指定し、 set_property コマンドから PUBLIC_HEADER プロパティを指定してインストール時にインストールすべきヘッダを指定して、 target_include_directories で自身のヘッダファイルの場所を指定して、 install コマンドでインストール先を指定している。あとはこの辺の単語でググれば最終的にはリファレンスにぶつかって解決できると思う。

  • CMakeLists.txt
project(sample_lib VERSION 0.0.1 LANGUAGES CXX)

add_library(sample_lib STATIC)
target_sources(sample_lib 
        PRIVATE
        # .cpp ファイルはここに書く
        )
set_property(TARGET sample_lib
        PROPERTY PUBLIC_HEADER
        # .hpp ファイルはここに書く
        )
target_include_directories(sample_lib 
        INTERFACE
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}> # ライブラリのビルド時に読み込むべき include ディレクトリ
        $<INSTALL_INTERFACE:include>) # ライブラリのインストール時(依存側で)読み込むべき include ディレクトリ
install(TARGETS sample_lib 
        EXPORT libsample_lib
        ARCHIVE DESTINATION lib
        LIBRARY DESTINATION lib
        PUBLIC_HEADER DESTINATION include)

利用する側

ExternalProject_add で CMakeLists.txt のあるディレクトリやレポジトリを指定すると、 cmake が走りよしなにビルドしたりインストールしてくれる。 INSTALL_DIR などでインストール先指定しているけれど、特段指定しなければデフォルトのディレクトリに突っ込まれる。が、 Windows 環境でやっていたのでインストールステップ時に C:\Program Files\ 下にライブラリをインストールしようとし、パーミッションが足りずに失敗する罠に引っかかったりするの利用側で指定してしまうのが、別段深い知見はないのだが、良いのではと思った。

  • CMakeLists.txt
project(test_app)

add_executable(test_app main.cpp)

include(ExternalProject)
## Dependencies
ExternalProject_add(sample_lib
        PREFIX ${CMAKE_CURRENT_BINARY_DIR}/sample_lib
        GIT_REPOSITORY https://path/to/sample_lib
        GIT_TAG master
        INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}
        CMAKE_ARGS "-DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR}"
        )
add_dependencies(test_app sample_lib)
link_directories(test_app PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/lib)
target_include_directories(test_app PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/include)
target_link_libraries(test_app PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/lib/libsample_lib.a)

あとがき

別段ベストプラクティスではないと思うので動くもの書いたくらい。 lib とか .a とか cmake 側でちゃんと生成して作ったほうがいいし、そも add_subdirectory と差があるんだっけとか、たぶん改善ポイントはすごく多い。 あと、今回作ってて依存側が依存先のことを超絶細かく知っていてもおかしくないという作りに本能が納得するまですごく時間がかかった。仕事では利用先のライブラリの名前とバージョン書いたら利用できることが多く、ここまでビルドやインストールの中身を意識したのが久々でたいへんつらかった。

コード書いてるもののクラス図を書いておく

ポエム

コード書いてたら何やってるのかわからなくなったのでクラス図を書き残したくなったがノートがないのでブログに書くことにした。

何するの

これのクラス図を適当に書いておく。書かないと 3 日で忘れるし。

shurabap.hatenablog.jp

クラス図

f:id:shurabaP:20200509180518p:plain

interface SynthesizePhrase {
  bool operator()(PhaseSignal*, PhraseParameters*)
}

class SynthesizePhraseWithWORLD {
  bool operator()(PhaseSignal*, PhraseParameters*)
}

SynthesizePhrase <|-u- SynthesizePhraseWithWORLD

interface SynthesizeImpulseResponse{
  bool operator()(ImpulseResponse *output, const ImpulseResponseParameters *input)
}

class SynthesizeImpulseResponseWithWORLD {
  bool operator()(ImpulseResponse *output, const ImpulseResponseParameters *input)

  const int fftSize
  const int samplingFrequency
}

SynthesizeImpulseResponse <|-- SynthesizeImpulseResponseWithWORLD
SynthesizePhraseWithWORLD .r.> SynthesizeImpulseResponse


class PhraseParameters {
  Spectrum *spectrogram[1..n]
}

class PhraseSignal {
  double *waveform
}

SynthesizePhrase -l- PhraseParameters
SynthesizePhrase -l- PhraseSignal

main .d.> PhraseSignal
main .d.> SynthesizePhraseWithWORLD
main .d.> PhraseParameters
main .d.> SynthesizeImpulseResponseWithWORLD

cmake の ExternalProject_add と add_subdirectory を利用して gtest を導入した。

ポエム

しばらく C++ を書いていなかったが趣味は C++ でやりたくなったので gtest を導入してテストを書けるようにしたくなった(テストを書くとは言っていない)。

やりたいこと

googletest を手動インストールしないで cmake 側からよしなに依存を解決したい。

方針

googletest のレポジトリの方針の通り、 add_subdirectory を利用して関連付けを行う。

googletest/README.md at master · google/googletest · GitHub

README にはこんなアツい思いが書かれている。

すでに CMake を利用しているプロジェクト内で gtest を使うのであれば、 gtest をそのプロジェクトの一部として直接ビルドしてしまうのが、ロバストで柔軟性の高いアプローチだ。このアプローチは、 GoogleTest のソースコードを メインのビルドでアクセス可能にし CMake の add_subdirectory()) コマンドで GoogleTest を追加することで実現できる。こうすることで、 gtest と自身で書いた残りのプロジェクトとでコンパイラとリンカの設定が同一のものを利用できるようになり、例えば debug/release のような、互換性のないライブラリを利用したことによる問題を回避できるといった素晴らしいメリットを得られる。特に Windows では顕著に有用である。GoogleTest のソースコードをメインのビルドで利用するにはいくつかの方法がある。

  • GoogleTest のコードを手動でダウンロードし既知の場所に保存する。これは最も柔軟性の低いアプローチであり、 CI などを利用するのを難しくしてしまう可能性がある。

  • メインのプロジェクトのソースツリーにGoogleTest のコードを直接コピーとして埋め込んでしまう。しばしば見られる最も単純なアプローチではあるが、最新の状態を保つのが最も難しい方法でもある。

  • git submodule かそれに準ずるもので GoogleTest を追加してしまう。この方法も常に取れるとは限らず適切とは限らない。例えば、 git submodule 自身に長所と欠点があるからだ。

  • CMake のビルドの configure step の一部として GoogleTest をダウンロードする。この方法はほんの少し複雑ではある、が、他の方法のような制限を受けない。

上にあげた方法の最後の方法を実現するには、分割した小さなファイル、例えば CMakeLists.txt.in とする、に、 CMake の stage 中にビルド内にコピーされ、当該のコードがビルドの一部として実行されるよう小さな CMake のスクリプトを書けば良い。

なんかたくさん地雷こと踏んだのかね?ぼくも前回はローカルに直接インストールしたけど頭痛いのでそれはもうやりたくないしこの方法を使ってみる。なお、 cmake のバージョンは 2.8.2 以降が要求されている。

やったこと

README.md に書いたことをそのままやった。

ディレクトリ構成

-+--- ./
 +--- CMakeLists.txt
 +-+- src/
 | +- CMakeLists.txt
 | +- Target.cpp
 | +- Target.hpp
 |
 +-+- test/
   +- CMakeLists.txt.in
   +- CMakeLists.txt
   +- main.cpp
   +- TargetText.cpp

./CMakeLists.txt

プロジェクトルートは子ディレクトリを追加する以外何もしない。

project(app_root)

add_subdirectory(src)
add_subdirectory(test)

./src/CMakeLists.txt

テスト対象となるコードはいつもどおり書けば良い。特段難しいことはしない。

project(app)

# add_library(app) なりなんなりいつものように書く

./test/CMakeLists.txt.in

GoogleTest を CMake が作業ディレクトリ内でよしなに展開するようにしている。あんまりポイントはないが、ソースコードの置き場が ${CMAKE_BINARY_DIR}/googletest-src になり、a ファイルの置き場が ${CMAKE_BINARY_DIR}/googletest-build になることだけ気をつけるといい。このコマンドは GoogleTest をダウンロードするがほかはなにもしない。

cmake_minimum_required(VERSION 2.8.2)
project(googletest-download NONE)

include(ExternalProject)
ExternalProject_Add(googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG master
    SOURCE_DIR "${CMAKE_BINARY_DIR}/googletest-src"
    BINARY_DIR "${CMAKE_BINARY_DIR}/googletest-build"
    CONFIGURE_COMMAND ""
    BUILD_COMMAND ""
    INSTALL_COMMAND ""
    TEST_COMMAND ""
)

./test/CMakeLists.txt

テストの実行側で GoogleTest に CMake を実行し GoogleTest のダウンロード先のディレクトリを add_subdirectory()コンパイル対象に加える。

project(app_test)

# Download and unpack googletest at configure time
configure_file(CMakeLists.txt.in googletest-download/CMakeLists.txt)
execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" .
        RESULT_VARIABLE result
        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/googletest-download )
if(result)
    message(FATAL_ERROR "CMake step for googletest failed: ${result}")
endif()
execute_process(COMMAND ${CMAKE_COMMAND} --build .
        RESULT_VARIABLE result
        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/googletest-download )
if(result)
    message(FATAL_ERROR "Build step for googletest failed: ${result}")
endif()

# Prevent overriding the parent project's compiler/linker
# settings on Windows
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)

# Add googletest directly to our build. This defines
# the gtest and gtest_main targets.
add_subdirectory(${CMAKE_BINARY_DIR}/googletest-src
        ${CMAKE_CURRENT_BINARY_DIR}/googletest-build
        EXCLUDE_FROM_ALL)

# The gtest/gtest_main targets carry header search path
# dependencies automatically when using CMake 2.8.11 or
# later. Otherwise we have to add them here ourselves.
if (CMAKE_VERSION VERSION_LESS 2.8.11)
    include_directories("${gtest_SOURCE_DIR}/include")
endif()

# あとは GoogleTest を普通に使うだけでいい。

おわりに

英語読めば一発なんだが頭が弱っているから読むのにも時間かかる…とりあえず GoogleTest に限らず自作のライブラリとかにも応用できそうな気はした。しかしまあ CMake は便利なんだけど、結局ビルドプロセスを頭に入れないとかー、という気持ちにはなった。

UTAU 音源読み込む単純なライブラリ書いた

ポエム

GW に向けて暇だったので作った。いままで Qt だよりにしていたんだけれど、小さい部分はなるべく依存が小さくなるよう人生を悔い改めたので小さく作っている。

github.com

作ったもの

#include <iostream>
#include "Voicebank.hpp"

int main() {
    const char *voicebankDir = "/path/to/utau/voicebank";
    std::cout << uzume::utau::Voicebank::from(voicebankDir).to_string();
    return 0;
}

信号処理しないので簡単。とりあえず小春音アミさんの17音階連続音読めたしテストないけど動くでしょ。

www14.big.or.jp

いまさら WORLD の合成部分の薄い C++ ラッパー作った

ポエム

世間が何かと騒がしく外に出るのもままならなくなった反面、在宅勤務で体力が回復してきたので忘れていたコーディングをし直している。とりあえず勘を取り戻そうかと表題の通り音声合成でもやってみる。本職は web エンジニアだったはずなんだがどうにもこういうもののが書きやすいのは変わらないようだ。

やりたいこと

WORLD は C のインタフェースとしてとても優秀でオフライン合成であれば(※)割とさっくり分析・合成ができるのだが、抽象度あげようとするとどうしても C の構文がぼくの頭には難しいので少し抽象度上げたかった。また久しく C++ やっていなかったので弱った脳みそでもなんとかなる合成部分だけやることにした。やりたいことは下記。 (※:オンライン合成もサンプルがあってたぶん便利なのだろうけど使ったことがないので使い勝手はわからない。)

  • 時刻を言うと F0 を返事してくれる人がいる
  • 時刻を言うとそのときのスペクトルを返事してくれる人がいる
  • このとき上の 2 つの人の言うことを聞きながらいい感じの合成音声を再生成したい

まんまこれがインタフェースなのでそのまま実装すればいいのだが、再生成部分のインタフェースが何言ってんのかわからずにかわいくなくなりそうだったので、再生成は以下の 2 つの人に役割を分ける。

  • ある時刻のスペクトル(精確には周期性成分のスペクトルと非周期性成分のスペクトル)を言うとそのスペクトルのインパルスレスポンスを返事してくれる人
  • インパルスレスポンスをいい感じに並べて一連の音声にしてくれる人

これで難しいことはなにもない。数学?今回は全部 WORLD に任せるよ。

作った

github.com (どうでもいいけどアメノウズメさんから名前借りたけど、芸術の神というより宴会芸の神のイメージが強いから名前違うほうがいいかしら、まあプロトタイプだしなんでもいいか)

    int fftSize = 1024;
    int f0Length = 1000;
    NaiveSpectrogram spectrogram((unsigned int)f0Length, (unsigned int)fftSize, /* msFramePeriod = */ 1.0);

    /* Analyze and create WORLD spectrogram here. */

    int waveLength = 44100;
    int samplingFrequency = 44100;
    double *wave = new double[waveLength];
    for(int i = 0; i < waveLength; i++) {
        wave[i] = 0.0;
    }

    SynthesizeImpulseResponseWithWORLD irs(spectrogram.fftSize(), samplingFrequency);
    SynthesizePhraseWithWORLD synthesize(&irs, /* f0Floor = */ 71.0, /* f0Default = */ 500.0);

    PhraseSignal s(wave, /* indexMin = */ 0, /* indexMax = */ waveLength, samplingFrequency);
    PhraseParameters p(&spectrogram, /* startPhase = */ 0.0, /* startFractionalTimeShift = */ 0.0);

    synthesize(&s, &p);

    /* Save wave as wav file here. */

最初の 2 つの F0 とスペクトルのお返事をするクラスが class NaiveSpectrogram final : public Spectrogram さん、インパルスレスポンスをお返事する人が class SynthesizeImpulseResponseWithWORLD final : public SynthesizeImpulseResponse さん、お返事まとめて音声にしてくれる人が class SynthesizePhraseWithWORLD final : public SynthesizePhrase さん。クラスの名前は長いがこんな感じで合成部分は synthesize(&s, &p); にまとまる。ここまで書くのに二週間弱くらい。趣味をやらんくなる寸前までこの部分やれなくて四苦八苦してたので当時よりはいくぶんか成長はしたようだ。DIO, CheapTrick, , D4C, StoneMask で分析したデータを spectrogram に突っ込んでおけばよしなに再合成してくれる動作確認はしたがテストは書いていない、というかバイナリのテストの書き方がわからんのでお手上げという感じ。一応テスタブルには書いてあるので頑張ればなんとかなるけど趣味だしまあ。

UTAU 音源を読み込む Go のコードを書いてみた

ポエム

本当に今さらなのだけれど Go を触ることになりそうなので手馴れている処理を Go で書いてみることにした。本職 Web エンジニアの割にえらいネイティブアプリ寄りなんだよなと思いつつも UTAU 音源読むところがちょうどよさそうなので実装してみた。

レポジトリ

github.com

超久々に個人レポジトリを更新した。 4 年くらい趣味プロしていなかったのか、フェルマータにしても休みすぎでしょう。

やったこと

shurabap.hatenablog.jp

これも 6 年前か。とりあえずこの辺のデータ構造をコードに起こした。

あんまりはまらなかったけれど、 function を mock できなさそうな雰囲気を感じたので、 interface に書き直すことにはなった。自前で mock を書けばいいんだろうけど、さすがに何回どんな引数で呼ばれたかを自分で書くのは苦しい。最初はこんな感じで書いていた。

// CharacterReader reads Character from filesystem.
type CharacterReader interface {
    Read(string) (*Character, error)
}

// CharacterReaderDefault is a default CharacterReader.
type characterReaderDefault struct {
    fileRead func(string) (string, error)
    characterFactory func(string) (*Character, error)
}

// NewCharacterReader creates a default CharacterReader that reads Character from filesystem.
func NewCharacterReader() CharacterReader {
    return characterReaderDefault{
        fileRead : fileRead,
        characterFactory : NewCharacter,
    }
}

CharacterReader の実装クラスは character.txt を読むことになるのでファイル読み込みをする関数を DI できるようにし、また Character のファクトリも DI できるようにしておいた。単体テストのためで、後々モック化しようと思ったが良い方法が見つからず、結局 testify を導入して interface を注入するように変えた。

// CharacterReader reads Character from filesystem.
type CharacterReader interface {
    Read(string) (*Character, error)
}

// CharacterReaderDefault is a default CharacterReader.
type characterReaderDefault struct {
    fileRead fileReader
    characterFactory characterFactory
}

// NewCharacterReader creates a default CharacterReader that reads Character from filesystem.
func NewCharacterReader() CharacterReader {
    return characterReaderDefault{
        fileRead: fileReaderDefault{},
        characterFactory: characterFactoryDefault{},
    }
}

これで良し。とりあえず一通り触ることを優先したのでテストコードまで書いたけど動作検証はしていない。もともと BASIC -> C -> C++ -> C++ with Qt と来ているので文法は慣れれば気にならなかった。

参考

github.com