読者です 読者をやめる 読者になる 読者になる

ボレロ村上 - ENiyGmaA Code

中3女子です。

constexpr で音階生成&シンセサイザー&音声合成

中3女子です。
このエントリは C++ Advent Calendar 2012 の 7 日目の記事です。

概要

この記事では Sprout C++ Library という拙作のライブラリによって、以下のようなプログラムを作成する。

    1. コンパイル時音声処理ライブラリ Sprout.Compost の紹介
    2. コンパイル時に基本波形による音階を生成する
    3. コンパイル時に波形にエフェクトをかける
    4. コンパイル時に音声合成する


なお、自分は音楽理論やサウンドプログラミングに関してまったく素人であり、その方面の用語や解説の不正確な部分についてはご容赦願いたい。

動作環境

本記事内のコードは下記の環境でコンパイル・実行を行なっている。

自分はいつも GCC をメインに開発しており、これは constexpr の準拠度が高くビルトイン数学関数のような有用な拡張もあるためである。
しかし、今回 Clang を使ったのは理由がある。


今回は音声処理ということで、44.1kHz であれば秒あたり 44100 個の要素を処理する必要がある。
ところが GCC ではコンパイル時に巨大な配列(数万要素)を作成しようとするとコンパイルエラーになる。
また、メモ化によるメモリの大量消費の問題もある。
その点について Clang は問題なく動作するため、今回採用することにした。

共通ヘッダ

実行時に処理しなければならない機能(実際に音を鳴らしたりファイルに出力したり)は、以下のヘッダにまとめてある。

    • ヘッダ (wave_io.hpp)

コンパイル時音声処理ライブラリ Sprout.Compost の紹介

Sprout.Compost は、constexpr による Range アダプタベースのコンパイル時音声処理ライブラリである。


Sprout C++ Library については、 @manga_osyo 氏の記事で既に紹介いただいているので、ここでの説明は省略する。
【C++ Advent Calendar 2012】Sprout を使うたった一つの理由【2日目(前編)】 - C++でゲームプログラミング
なお、プロジェクトのリポジトリはここ【bolero-MURAKAMI/Sprout · GitHub】にある。

Range アダプタとは

読者諸賢には今更説明の必要もないと思われるが、一応。


Range とは、あるデータの範囲を表すコンセプトであって、C++ では始点と終点を指すイテレータの組として実装されるのが一般的である。
代表的な Range アダプタのライブラリは、Boost.Range や Pstade.Oven などがある。


Sprout が規定する最小の Range の実装は以下のようなインタフェースである。

struct MyRange {
    typedef X iterator;
    iterator begin();
    iterator end();
};

このような、始点と終点を指すイテレータを得ることのできる型は Range として扱うことができる。
例えば std::vector や std::string などは Range である。
もちろん、constexpr で使用することができるのは、sprout::array のようなリテラル型だけである。


また、Range アダプタとは、Range に対する遅延評価的な操作であり、複数の操作を合成することのできる機能である。

constexpr auto r = rng | reversed | transformed(nagete<>());

上記の reversed や transformed のように、| 演算子によって Range を別の Range へ変換するコンセプトを Pipable Adaptor という。
例えば reversed は std::reverse のように元の範囲を書き換えるのではなく、[ reversed_iterator(end(rng)) .. reversed_iterator(begin(rng)) ) のような、
範囲を逆順に辿るイテレータの組 reversed_range を生成する。
reversed_range それ自体もまた Range であるから、更に他のアダプタを適用することができる。


このように、Range アダプタの適用は一般に元の Range への副作用を持たない。
これは関数型言語由来の設計思想であるが、constexpr においては重要な意味を持つ。

constexpr auto r1 = sprout::reverse(rng);
constexpr auto r2 = sprout::transform(r1.begin(), r1.end(), r1, nagete<>());

constexpr アルゴリズムを Range アダプタを使わずに書くと上記のようになる。
constexpr では副作用を持たせられないから、範囲を書き換えるような操作をするには、別の新たな範囲を生成しなければならないからだ。
Range アダプタを活用することによって、constexpr で大きな範囲を扱う場合でもテンポラリを生成することなく、効率的に処理することができる。

コンパイル時に基本波形による音階を生成する

Sprout.Compost には、正弦波、矩形波三角波、ノコギリ波やホワイトノイズなどの基本的な波形を生成する機能がある。
今回は特に正弦波を用いて話を進める。

まずは単音の正弦波を生成する
    • コード (sine_sound.cpp)

ここで使われた Range アダプタは下記である。

sinusoidal(frequency = 1, amplitude = 1, phase = 0)

sinusoidal アダプタは、指定した周波数、振幅、位相の正弦波の範囲を生成する。
なお、パイプされた場合は左辺の範囲全体を fill するような挙動となる。

as_pcm_wave16

as_pcm_wave16 アダプタは、浮動小数点の範囲 [-1.0 .. 1.0] を WAVE 形式(16bit符号付き整数)に変換する。

copied

copied アダプタは、範囲を任意のコンテナへ暗黙変換できるようにする。
また equal_temperament_value は、平均律において基音から指定半音数ずらした周波数の倍率を求めるユーティリティである。


なお、このプログラムはデフォルトで 440Hz の波形(ラ音)を生成するが、CMP_SEMITONES を定義することで任意の音階を生成できる。
例えばコンパイルオプションで -DCMP_SEMITONES=3 とすれば、平均律で『ラ』から 3 半音ずらした 523.25Hz の『ド』音を出力する。

time clang++ -o sine_sound sine_sound.cpp -DCMP_LENGTH=2 -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
real    8m20.392s
user    8m17.211s
sys     0m1.364s


『ピー』という正弦波特有の音色(純音)が確認できたと思う。
さて、これでコンパイル時に音声波形が生成できることは確認できたが、これではあまりに味気ない。
特定音階を出すことには既に成功しているので、次は音階を繋げてみよう。

ドレミファソラシドの音階を生成する
    • コード (sine_cdefgabc.cpp)

ここで新たに使われた Range アダプタは下記である。

taken(n)

taken アダプタは、範囲の先頭から指定個数の要素を取り出した範囲を生成する。

jointed(range)

jointed アダプタは、左辺の範囲の後ろに右辺の範囲を結合した範囲を生成する。
ここでは、ド、レ、ミ、ファ、ソ、ラ、シ、ド、の 8 音をそれぞれ繋げて結果に格納している。

time clang++ -o sine_cdefgabc sine_cdefgabc.cpp -DCMP_LENGTH=0.3 -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
real    131m14.359s
user    130m50.595s
sys     0m1.884s


『ピピピピピピピピ↑』という感じの音階が聞こえたと思う。
音階を繋げることができたので、適当に繋げればメロディをつくることもできるだろうことは想像できる。
しかしながら、音楽にはまだ重要な要素がある。和音である。
次は、単音を重ね合わせて和音を生成してみよう。

ドミソの和音を生成する
    • コード (sine_ceg.cpp)

ここで新たに使われた Range アダプタは下記である。

superposed(range)

superposed アダプタは、左辺の範囲と右辺の範囲を重ね合わせた(加算した)範囲を生成する。
ここでは、ド、ミ、ソ、の 3 音をそれぞれ重ね合わせて結果に格納している。

time clang++ -o sine_ceg sine_ceg.cpp -DCMP_LENGTH=2 -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
real    22m57.638s
user    22m53.110s
sys     0m1.004s


メロディも無いので特に面白いものではないが、音の重ね合わせも表現できることは確認できた。
さて、基本はできたのでいよいよ音を組み合わせて適当なメロディをつくってみよう。

モーツァルトのきらきら星変奏曲ハ長調K. 265の最初の部分のメロディをつくる
    • コード (sine_performer.cpp)

最初はソースに音符の情報をハードコーディングしようと考えたが、メモリが全く足りず Clang が bad_alloc を吐いて落ちたので方針変更。
コンパイルオプションの -D で楽譜情報をマクロとして渡すことにした。
CMP_SCORE0_NUM, CMP_SCORE0 が(休符も含めた)音符の数、および音符のリストである。これは楽譜の上段の列の分。
楽譜の下段の列は同じように CMP_SCORE1_NUM, CMP_SCORE1 で表す。


間に無音部分を挟んでいるのは、完全に連続していると、同じ音を続けて鳴らす箇所で一繋ぎになってしまうからである。
本当は適当に減衰をかければそれらしくなるだろうが、時間(コンパイル時間)の関係で諦めた。


前述のように、一度にコンパイルしようとすると落ちるので、2 小節ごとに分けてコンパイルする。

time clang++ -o sine_twinkle0 sine_performer.cpp -DCMP_SCORE0_NUM=8 -DCMP_SCORE0="{{1./8,15,.6},{1./8,0,0},{1./8,15,.6},{1./8,0,0},{1./8,22,.6},{1./8,0,0},{1./8,22,.6},{1./8,0,0}}" -DCMP_SCORE1_NUM=8 -DCMP_SCORE1="{{1./8,-9,.2},{1./8,0,0},{1./8,3,.2},{1./8,0,0},{1./8,7,.2},{1./8,0,0},{1./8,3,.2},{1./8,0,0}}" -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x

time clang++ -o sine_twinkle1 sine_performer.cpp -DCMP_SCORE0_NUM=8 -DCMP_SCORE0="{{1./8,24,.6},{1./8,0,0},{1./8,24,.6},{1./8,0,0},{1./8,22,.6},{1./8,0,0},{1./8,22,.6},{1./8,0,0}}" -DCMP_SCORE1_NUM=8 -DCMP_SCORE1="{{1./8,9,.2},{1./8,0,0},{1./8,3,.2},{1./8,0,0},{1./8,7,.2},{1./8,0,0},{1./8,3,.2},{1./8,0,0}}" -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x

time clang++ -o sine_twinkle2 sine_performer.cpp -DCMP_SCORE0_NUM=8 -DCMP_SCORE0="{{1./8,20,.6},{1./8,0,0},{1./8,20,.6},{1./8,0,0},{1./8,19,.6},{1./8,0,0},{1./8,19,.6},{1./8,0,0}}" -DCMP_SCORE1_NUM=8 -DCMP_SCORE1="{{1./8,5,.2},{1./8,0,0},{1./8,2,.2},{1./8,0,0},{1./8,3,.2},{1./8,0,0},{1./8,0,.2},{1./8,0,0}}" -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x

time clang++ -o sine_twinkle3 sine_performer.cpp -DCMP_SCORE0_NUM=6 -DCMP_SCORE0="{{1./8,17,.6},{1./8,0,0},{1./16*3,17,.6},{1./16,19,.6},{1./4,15,.6},{1./4,0,0}}" -DCMP_SCORE1_NUM=6 -DCMP_SCORE1="{{1./8,-4,.2},{1./8,0,0},{1./8,-2,.2},{1./8,0,0},{1./4,-9,.2},{1./4,0,0}}" -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
(初回はログをコピーするのを忘れたため記録なし)

real    82m22.732s
user    80m29.646s
sys     0m1.920s

real    78m47.133s
user    76m36.655s
sys     0m3.516s

real    20m57.575s
user    20m27.945s
sys     0m0.980s

4 回に分けてコンパイルしたものを一つに繋ぎあわせたものである。
聞き覚えのあるきらきら星のメロディが確認できたと思う。
当然ながらいかにも電子音という感じのピコピコした艶のない音だが、いちおう音楽にはなっている。


ともあれ、Sprout.Compost によってコンパイル時にメロディの波形を生成することができることは確認された。
音階生成についてはひとまずこれで区切りとして、次の項目に移ることとする。

コンパイル時に波形にエフェクトをかける

Sprout.Compost には、波形に対してシンセサイザーで使われるようなエフェクトを適用する機能がある。
例えばリバーブ、オーバードライブ、ファズ、コンプレッサ、ビブラート、ノイズゲートなどが実装されている。
今回は特にディストーショントレモロ、コーラスを取り上げる。

ディストーションをかける

エフェクトの説明の前に、ここではじめて出てきた機能について解説しておく。

#   define COMPOST_DEF_LOAD_SOURCE_IDENTIFIER wav
#   define COMPOST_DEF_LOAD_INFO_IDENTIFIER wav_info
#   define COMPOST_DEF_LOAD_SOURCE_FILE SPROUT_PP_STRINGIZE(CMP_FILENAME)
#   include COMPOST_LOAD_SOURCE

上記の部分は、Sprout.Compost外部ファイルから波形データを読み込む機能である。


COMPOST_DEF_LOAD_SOURCE_IDENTIFIER マクロは、読み込んだデータを格納する変数名を定義する。
COMPOST_DEF_LOAD_INFO_IDENTIFIER マクロは、読み込んだデータの各種情報を格納する変数名を定義する。これは省略可能。
COMPOST_DEF_LOAD_SOURCE_FILE マクロは、読み込む外部ファイル名を定義する。
以上を定義した上で include COMPOST_LOAD_SOURCE すれば、波形データが読み込まれる。
基本的な原理的は、よくある以下のようなコードと同じである。

int data[] = {
#   include "data.csv"
};


もちろん、読み込むファイルは Sprout.Compost の要求に合致するフォーマットでなければならない。
以下は、WAVE ファイルから Sprout.Compost 用の入力形式に変換する簡単なツールのソースである。

    • WAVE 形式から hpp への変換 (wavconv.cpp)


wavconv ツールを実行することで、以下のようなファイルを生成することができる。

distortion_input.wav.hpp - Gist (4万行以上あり極めて重いので埋め込みしていない。リンク注意)


話を戻そう。
ここで新たに使われた Range アダプタは下記である。

distorted(gain, level)

distorted アダプタは、範囲に対してディストーションをかけた範囲を生成する。
ディストーションの実装は以下のようなものである。

    1. 波の高さを gain 倍する。
    2. 一定のレベル [-1.0 .. 1.0] を越えた部分をクリップする。
    3. 波の高さに level を掛けて大きさを調整する。
time clang++ -o distortion distortion.cpp -DCMP_FILENAME="distortion_input.wav.hpp" -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
real    6m33.537s
user    5m45.722s
sys     0m45.851s

元の音とディストーションをかけた後の音を繋げたものである。
『チャッチャチャーン♪』という音に歪みが加わって『ジャッジャジャーン!』という感じになっている。
ヘヴィメタルなどではこのディストーションのエフェクトを多用する傾向にある。

トレモロをかける
    • コード (tremolo.cpp)


    • 入力ファイル (tremolo_input.wav.hpp)

tremolo_input.wav.hpp - Gist (4万行以上あり極めて重いので埋め込みしていない。リンク注意)

ここで新たに使われた Range アダプタは下記である。

tremolo(depth, rate, samples_per_sec = 44100)

tremolo アダプタは、範囲に対してトレモロをかけた範囲を生成する。
トレモロは音を depth[s] 深度で rate[Hz] の割合で音を揺らすエフェクトである。

time clang++ -o tremolo tremolo.cpp -DCMP_FILENAME="tremolo_input.wav.hpp" -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
real    10m0.510s
user    9m8.102s
sys     0m45.799s

元の音とトレモロをかけた後の音を繋げたものである。
『ジャーン♪』という音に揺れが加わって『ジャァ〜ン♪』という感じになっている。

コーラスをかける
    • コード (chorus.cpp)


    • 入力ファイル (chorus_input.wav.hpp)

chorus_input.wav.hpp - Gist (4万行以上あり極めて重いので埋め込みしていない。リンク注意)

ここで新たに使われた Range アダプタは下記である。

chorus(d, depth, rate, samples_per_sec = 44100)

chorus アダプタは、範囲に対してコーラスをかけた範囲を生成する。
コーラスは音を d[s] 範囲の depth[s] 深度で rate[Hz] の割合で音を広げるエフェクトである。

time clang++ -o chorus chorus.cpp -DCMP_FILENAME="chorus_input.wav.hpp" -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
real    21m46.729s
user    19m25.837s
sys     2m15.804s

元の音とコーラスをかけた後の音を繋げたものである。
『タタタターン♪』という音に広がりが加わって『ンタタタタ〜ン♪』という感じになっている。


以上のように、Sprout.Compost によってコンパイル時にシンセサイザーの様々なエフェクトをかけた波形を生成することができることが確認できた。
これらのエフェクトは、簡単な数式といくつかの基本的な Range アダプタの活用によって実装されているので、興味のある方はライブラリのソースを覗いてみては如何か。
さて、次は最後の項目、音声合成の話に入る。

コンパイル時に音声合成する

今回は、日本語の母音を発音する人の声(のように聞こえる音)を生成してみる。
本当は汎用な音声合成エンジンをライブラリ化して公開したかったが、残念ながら時間が足りなかったので現時点の実装を挙げる。

母音の音声を生成する
    • コード (vowel.cpp)

実装は見てのとおり簡単なもので、Rosenberg 波を音源波形として、フォルマントを元に IIR フィルタでレゾナンスを掛けることで音声を再現している。


Rosenberg 波は、時間を t, 声門開大期を τ1, 声門閉小期を τ2 として次式で表される。
f(t)=\begin{cases} 3(\frac{t}{\tau_1})^2-2(\frac{t}{\tau_1})^3 \quad 0 \leq t \leq \tau_1 \\ 1-(\frac{t-\tau_1}{\tau_2})^2 \quad \tau_1 \leq t \leq \tau_1+\tau_2 \end{cases}
このコードでは τ1 = 0.8, τ2 = 0.16 として計算している。
フォルマントとは音声のスペクトルに固有のピークのことである。
IIR フィルタというのは、無限インパルス応答において特定の周波数成分を取り出すためのフィルタである。
このあたりのことについて筆者は無知なので深い説明は避ける。


定義について詳しくは文献や論文を参照いただくとして、ここでは処理を constexpr で実装する上での要点に触れる。
Rosenberg 波は正弦波のような周期関数と同様に生成できるから問題ない。
フォルマントは適当な値を与えてやればよい。
IIR フィルタは定義に基づいて設計してやればよい。
実装上で問題になるのは、IIR フィルタの適用である。


離散時間 IIR フィルタは入力信号と出力信号をパラメータにとる差分方程式で表される。
これはつまり、正弦波のように時間をとれば値が定まるものとは異なり、先頭から順次的に求めなければならないということを示している。
それが何故問題になるかというと、constexpr 関数の再帰深度の問題である。

アルゴリズム再帰深度を低減する

処理フローにおけるループは、constexpr ではふつう再帰で実装される。
C++11 の規格では、constexpr 関数の呼出の深さは少なくとも 512 まで許容するようコンパイラに推奨している。
この値は処理系定義でコンパイラ毎に規定され、あるいはコンパイルオプションで指定できるが、いずれにせよこの深度制限を超えるとコンパイルエラーになる。


さて、44100Hz×1sec = 44100 個の値を再帰で順次求めて結果の配列に格納すれば、初歩的な実装では再帰の深度は当然 44100 になる。
これでは明らかにコンパイルエラーになる。
GCC や Clang では -fconstexpr-depth オプションで深度制限を指定できるが、数千程度ならともかく数万になると、スタックが溢れるかメモリを使い切るかしてコンパイラが落ちる。


そのためこのコードでは、データを一定数のブロック毎に分割して処理している。(detail::generate_vowel_block の部分)
ブロック毎に結果を返して再帰元へ戻るため、(BlockSize * K) の再帰深度を (BlockSize + K) に低減することができる。
例えば 要素数が 44100 = (225 * 196) の場合、(225 + 196) = 421 という具合になる。


ところで、この処理に関しては上記の方法しか現時点で無いが、この方法では再帰深度のオーダーが高々 O(N) → O(√N) 程度にしか低減できないし、
返値の型も制限ができて汎用性が落ちるため、万事に適用できる手法ではない。

特に基本的な数学関数やアルゴリズムの実装では、より再帰深度を低減する方法が求められる。

アルゴリズム再帰深度をもっと低減する

いわゆる積算処理について考える。
マクローリン展開で値を求めたり、accumlate などの処理にあたる。

これは、線形再帰を二分再帰に変えることで、再帰深度を高々 O(N) → O(logN) に低減することができる。
例えば sprout::cos の現在の実装では、マクローリン展開の 85 項までを積算しているが、再帰深度は 7 程度である。
Sprout/sprout/math/cos.hpp at master · bolero-MURAKAMI/Sprout · GitHub

注意すべきは、この方法は結合法則が成立する演算でなければならず、除算のように順序を変えられない場合は使えない。
そのため sprout の accumulate は、演算順序が安定なものとそうでないものと二つを用意したりしている。
Sprout/sprout/numeric/accumulate.hpp at master · bolero-MURAKAMI/Sprout · GitHub
https://github.com/bolero-MURAKAMI/Sprout/blob/master/sprout/numeric/unstable_accumulate.hpp

      • 2013/01/03 追記

研究の結果、順序を変えずに対数オーダーの再帰深度でアルゴリズムを実装できることがわかった。
(このため、unstable 版は不要であるので削除する)

アルゴリズムでそもそも再帰しない

範囲を取って別の範囲を返す処理を考える。
つまり範囲を書き換えたりコピーしたりするアルゴリズムにあたる。
このうち、全単射かつランダムアクセス、また出力範囲のサイズがコンパイル時定数であるような変換は、再帰なしに実装することができる。

例えば sprout::transform のランダムアクセス版などは、実装に IndexTuple イディオムを用いることで再帰を無くしている。
Sprout/sprout/algorithm/fixed/transform.hpp at master · bolero-MURAKAMI/Sprout · GitHub
IndexTuple イディオムについては【中3女子でもわかる constexpr】でも簡単に解説している。

time clang++ -o vowel_a vowel.cpp -DCMP_VOWEL=a -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
time clang++ -o vowel_i vowel.cpp -DCMP_VOWEL=i -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
time clang++ -o vowel_u vowel.cpp -DCMP_VOWEL=u -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
time clang++ -o vowel_e vowel.cpp -DCMP_VOWEL=e -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
time clang++ -o vowel_o vowel.cpp -DCMP_VOWEL=o -D__STRICT_ANSI__ -Wall -pedantic -std=gnu++0x
real    86m18.537s
user    83m10.872s
sys     1m22.825s

real    85m28.002s
user    82m23.045s
sys     1m22.629s

real    87m55.815s
user    84m34.705s
sys     1m23.205s

real    89m36.108s
user    85m55.018s
sys     1m28.506s

real    88m8.639s
user    84m25.013s
sys     1m34.302s

聴いてのとおり「並べてみると、言われれば『あいうえお』と聞けなくもない」程度である。
ここから更に人間らしい声にしていくには様々な調整が必要になる。


ひとまず今回はここまでで、子音との組み合わせや汎用エンジンの設計などは次回の課題としたい。

まとめ

これまで見てきたように、constexpr によるプログラミングは、

    1. コンパイラのバグ
    2. コンパイル時間
    3. メモリ不足
    4. 再帰制限

との戦いである。


念のため言っておくが、驚くべきことに constexpr はこのような音声処理や レイトレーシング のためにある機能ではないらしい。
constexpr の機能としての実用性は、

    1. 最適化
    2. メタプログラミングの支援
    3. 参照透過性の保証
    4. グローバルオブジェクトの Zero-initialization

などとされる。


では Sprout C++ Library の意義は何かと言えば、実用的な観点で見れば

    1. STL など既存ライブラリの constexpr 化の検証
    2. イディオムの研究
    3. コンパイラのベンチマーキング

などが挙げられるだろう。
しかしながら、Sprout はそうしたモチベーションに基づいて実装されたものではない。


constexpr の実装上の制限と実現可能な機能が適度なバランスであり、副作用を持てないため自然と関数型言語的発想を働かせなければならない事もあり、
また GCC や Clang など主要なコンパイラには早い段階で実装されたがいまだ完全なものでなく、複雑で大量の処理を行うには処理系の挙動を推測しなければならない点も丁度よい。
(なお、VC++ は滅ぶべきであると考える次第である)
このように constexpr は、TMP や Cプリプロセッサと同様に、プログラミングを楽しむのに実に適した機能である。


願わくは、この記事があなたに constexpr への興味を抱かせ、または関心を深めるきっかけになりますよう。



C++ Advent Calendar 2012 明日のエントリは @yak_ex 氏です。
よろしくお願いします。【「や」の字