コード書いてるもののクラス図を書いておく
ポエム
コード書いてたら何やってるのかわからなくなったのでクラス図を書き残したくなったがノートがないのでブログに書くことにした。
何するの
これのクラス図を適当に書いておく。書かないと 3 日で忘れるし。
クラス図
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 だよりにしていたんだけれど、小さい部分はなるべく依存が小さくなるよう人生を悔い改めたので小さく作っている。
作ったもの
#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音階連続音読めたしテストないけど動くでしょ。
いまさら 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 音源読むところがちょうどよさそうなので実装してみた。
レポジトリ
超久々に個人レポジトリを更新した。 4 年くらい趣味プロしていなかったのか、フェルマータにしても休みすぎでしょう。
やったこと
これも 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 と来ているので文法は慣れれば気にならなかった。
参考
cmake で github のレポジトリを登録したがコンパイルできない
経緯
Windows で CLion 使った開発を趣味でしているが表題の件でハマった。どういう状況でハマったかを書く。
- コンパイラは MinGW を使用している
- Git は Windows でも Linux コマンドが使えるようなパスを通している
- cmake を使って github 上のレポジトリを ExternalProject_Add した
上記の食合せだと動かなくなる。
どうなるのか
症例1
経緯の状態のまま cmake を実行して github のレポジトリを clone し、submodule update --init --recursive
的なことまでは進むが、 MinGW がコンパイルをボイコットする。
CMake Error at C:/path/to/CMake.cmake:20 (MESSAGE): sh.exe was found in your PATH, here: C:/path/to/sh.exe For MinGW make to work correctly sh.exe must NOT be in your path. Run cmake from a shell that does not have sh.exe in your PATH. If you want to use a UNIX shell, then use MSYS Makefiles. Call Stack (most recent call first): CMakeLists.txt:8 (project)
症例2
じゃあパスに入ってる sh.exe 消したら進むんじゃね?と浅はかに考えて sh.exe をリネームした。
fatal: 'submodule' appears to be a git command, but we were not able to execute it. Maybe git-submodule is broken?
(◞‸◟)
どうするの?
Git のインストール時に UNIX ツール群へのパスを通さないオプションがあるので Git 消して再インストールすれば良い。 これでぼくは二日間帰宅後の時間をだいぶ無駄にした。
作りたいもののポエム4
概要
4つ目、そろそろ実装に行きたい。
ところでこないだ真理を思い出したので設計が非常に単純になった。
唐突に真理を思い出したんだけど、音符一音一音に対するモーフィングとかいらないんだった。要件が一個落ちて簡単になるぞ。
— コーラデブ@shurabaP (@shurabaP) 2016, 2月 18
どういうことかというと、ある音符内でのモーフィングは実は不要で、 もし音に表情を付けたいのであれば音符ごとの強弱だけで十分まかなえるということ。 何か根拠があるわけではないけれどぼくは研究者ではないし、 気持ちとしては作曲者なので経験則に従いたい。
作りたいもの
- UTAU 音源より表現力の高い音源形式
- ↑のファイル読み書き部分
要件
- 既存リソースを活かすために UTAU 音源からインポートをできること
- 波形情報と演奏情報とキャラクタ情報は厳密に分けて設計すること
- 欲を言えば複数音源を自在に切り替え、あるいは混ぜられるようにすること(キャラクタの枠組みを越えるべき)
データ設計
segment (切りだされた波形)
segment は波形ファイルへの参照と波形の切り出し位置、伸長の位置を指定する。
{ "id": "segment0001" "path": "akasatana.wav", "begin": 500, "length": 500, "temporal_position" : 100, "front_fixed_range" : 100, "rear_fixed_range" : 100 }
- id: segment ID
- begin: 開始位置
- length: 波形長
- temporal_position: ビートの位置(先行発音相当)
- front_fixed_range: 前半の固定長(固定長相当)
- rear_fixed_range: 後半の固定長
mapping(発音・音程・Velocityと、波形との対応)
ある発音がある音程である Velocity で指定された際にどの波形をどの程度利用するのかというマッピングを表す。
{ "pronounce" : { "before" : "a" "this" : "k a" "after" : "s a" }, "segments": [ { "segment_id": "segment0001", "range" : { "begin" : { "velocity": 0, "note": 0 }, "end" : { "velocity": 127, "note": 63 } } }, { "segment_id": "segment0002", "note": { "range" : { "begin" : { "velocity": 0, "note": 64 }, "end" : { "velocity": 127, "note": 127 } } } } ] }
- pronounce: 発音
- before: 先行音の発音(optional)
- this: 発音
- after: 後続音の発音(optional)
- segments: マッピングされるセグメント群
- segment_id: セグメントの識別子
- range: このセグメントが使用される範囲
- begin: 範囲の開始位置
- end: 範囲の終了位置
途中でのモーフィングが要らないためこれだけで表現が可能である。 基本的に既存のサンプラーと全く同じ仕組みだが、発音による検索が必要になるイメージ。
メタ情報(音源の名前とか)
{ "id": "澪音此羽", "version": "1.0.0", "author": "Hal@shurabaP", "web": "http://blog.hatena.ne.jp/shurabaP", "icon": "example.png", "path_type": "url", "url": "http://blog.hatena.ne.jp/shurabaP/reine_koreha", "mappings": "example.mappings", "segments": "example.segments", "description": "例ね、これは。" }
細かい形式
- pronounce
${consonant} ${vowel}
の形式で表す。子音が存在しない場合は前半を省略する。
- velocity, note
- 0-127 の 128 段階の整数
- begin, length など時間の位置
- ミリ秒で記す、どうせボコーダーがあじゃこじゃやるのに窓幅とかあるし適当でええやろ
- path
- path_type
- file か url の文字列をとること
悩みどころ
segments, mappings を JSON の配列にするべきかそれともファイルごとにするべきか。
たぶんだけど一個にまとめておいて遅ければ分割するのを考えるほうがいい気がする。