いまさら 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 の配列にするべきかそれともファイルごとにするべきか。
たぶんだけど一個にまとめておいて遅ければ分割するのを考えるほうがいい気がする。
作りたいもののポエム3
概要
一昨日からの続きやるよ。
作りたいもの
- 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", "note": { "before": 60, "this": 60, "after": 60 }, "velocity": 64 }, { "segment_id": "segment0001", "note": { "before": 60, "this": 60, "after": 60 }, "velocity": 64 } ] }
- pronounce: 発音
- before: 先行音の発音
- this: 発音
- after: 後続音の発音
- segments: マッピングされるセグメント群
- segment_id: セグメントの識別子
- note: 音高
- before: 先行音の音高
- this: 音高
- after: 後続音の音高
- velocity: ベロシティ
根本的には note, velocity の二次元平面上のマッピングなのでもうちょっとマシな書き方がありそう。 結構空間が広いのでデータ量が爆発するのが難点。
pronounce
${consonant} ${vowel}
の形式で表す。子音が存在しない場合は前半を省略する。
作りたいもののポエム2
概要
昨日の続きやるよ。
作りたいもの
- UTAU 音源より表現力の高い音源形式
- ↑のファイル読み書き部分
とりあえず一旦ここまで書けたらいいこととする。(大きく作って死ぬことが多い気がする)
その他要件
- 既存リソースを活かすために UTAU 音源からインポートをできるようにする
- 波形情報と演奏情報とキャラクタ情報は厳密に分けて設計すること
- 欲を言えば複数音源を自在に切り替え、あるいは混ぜられるようにすること(キャラクタの枠組みを越えるべき)
ざっくりとした設計
segment (切りだされた波形)
segment は機械的な意味しか有しない。構成としては波形ファイルへの参照と波形の切り出し位置、伸長の位置を指定する以外の機能を持たない。
{ "segments": [ { "id": "segment0001" "path": "akasatana.wav", "begin": 500, "length": 500, "temporal_position" : 100, "front_fixed_range" : 100, "rear_fixed_range" : 100 }, { "id": "segment0002" "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: 後半の固定長
切り出しと伸長はこれだけあればいい。これ以上の情報は別の情報に切り分けるべきだ。 UTAU の oto.ini は音楽的コンテキストを含まないのでこの設定は UTAU で言えば oto.ini に等しい。
切り出し部分についてはこれで良い。このレイヤーには発音や音程など発音のコンテキストを混ぜてはいけない。
phoneme (発音と音符の一断片)
phoneme は音楽的な一断片を表す。切りだされた波形とそれに対するメタ情報からなる。
{ "phonemes": [ { "id": "phoneme0001" "segment_id": "segment0001", "context" : { "pronounce": { "previous" : "-", "this" : "a", "next" : "k a" }, "note" : { "previous" : 60, "this" : 60, "next" : 60 }, "tempo" : 120, "strength" : 0 } } }, { "id": "phoneme0002" "segment_id": "segment0002", "context" : { "pronounce": { "previous" : "a", "this" : "k a", "next" : "s a" }, "note" : { "previous" : 60, "this" : 60, "next" : 60 }, "tempo" : 120, "strength" : 0 } } } ] }
ここまでは妥当な感じがする。
作りたいもののポエム
いきさつ
最近なにもプログラムを書いていなかった気がしてつらくなってきたので、なにか書くお題でも用意すればいいんじゃないかと思い立った。
作りたいもの
- UTAU 音源より表現力の高い音源形式
- ↑のファイル読み書き部分
とりあえず一旦ここまで書けたらいいこととする。(大きく作って死ぬことが多い気がする)
その他要件
- 既存リソースを活かすために UTAU 音源からインポートをできるようにする
- 波形情報と演奏情報とキャラクタ情報は厳密に分けて設計すること
- 欲を言えば複数音源を自在に切り替え、あるいは混ぜられるようにすること(キャラクタの枠組みを越えるべき)
ざっくりとした設計
とりあえずこんな感じでどうだろうみたいなのを書いてみる。 おそらく仕様が長大になるのでどっかで分割するべき。
{ "waves": [ { "path": "akasatana.wav", "segment": [ { "begin": 500, "length": 500, "temporal_position" : 100, "front_fixed_range" : 100, "rear_fixed_range" : 100, "context" : { "pronounce": { "previous" : "-", "this" : "a", "next" : "k a" }, "note" : { "previous" : 60, "this" : 60, "next" : 60 }, "tempo" : 120, "strength" : 0 } }, { "begin": 500, "length": 500, "temporal_position" : 100, "front_fixed_range" : 100, "rear_fixed_range" : 100, "context" : { "pronounce": { "previous" : "-", "this" : "k a", "next" : "s a" }, "note" : { "previous" : 60, "this" : 60, "next" : 60 }, "tempo" : 120, "strength" : 0 } } ] } ] }
- begin: 開始位置
- length: 波形長
- temporal_position: ビートの位置(先行発音相当)
- front_fixed_range: 前半の固定長(固定長相当)
- rear_fixed_range: 後半の固定長
- context
- pronounce
- previous: 先発の前の発音
- this: この波形の発音
- next: 後続の発音
- note
- previous: 先発の前の音程
- this: この波形の音程
- next: 後続の音程
- tempo: 収録テンポ
- strength: 音の強さ(うーん)
- pronounce
context は分離した方がいいかもしれない。
TBD..