精密なテンポでの再生を考える (長文)
今回の内容はテンポ云々だけじゃなくて
離散的なデータの精度を上げるのにも使えるかも。
…というより、そっちの応用みたいなもんですが。
- 1 - 0 演奏の最小単位を得る (フレーム長の取得)
というわけで、
まずは譜面の作成者側からも見える
「フレーム長」を求めてみる。
MIDIだと「Timebase 1単位あたりの時間」がコレにあたる。
具体的には
四分音符の長さをbeat_timeとすると、
・beat_time = 60000000(μs) / tempo
・frame_time = beat_time / timebase
とすれば、結構簡単に求められる。
Generatioの方では
(前も書いたような気もするけど、)
・frame_time = 60000(ms) / (tempo * timebase)
としてフレーム長を出しています。
ちなみに「frame_time」自体は
double型なので、ミリ秒以下(?)の精度を持っています。
- 1 - 1 実際のデータ長を得る (時間からデータ長への変換)
で、1フレームあたりの時間が得られたので
これに
フォーマットに合わせた1サンプルあたりのデータ長
を積算してやれば
1フレームあたりのデータ長
を求められます。
Generatioの場合は
44100Hz / 16bit / stereo
で固定なので、
1秒あたりのデータ量をone_sec_lenとすると
・one_sec_len = 44100 * (16 / 8) * 2
・frame_len = one_sec_len / 1000 * frame_time
となります。
- 1 - 2 再生する際の最小の精度を得る (サンプル単位、ならびにbyte単位の精度の取得)
一応、ここまで出せば
データ長としては求められているので、
frame_lenバイト分だけのデータを作って、
再生用のキューに放り込んでやれば普通に鳴ってくれます。
ただ、
「再生速度に高い精度が求められるとき」や
「波形を出力したいとき」などは
これでは不十分な場合があります。
例えば、
「timebase = 48, tempo = 120」の場合、
・frame_time = 10.416666...(ms)
・frame_len = 1837(byte)
となります。
前述のフォーマットより、
1サンプルあたりのデータ長は
((16 / 8) * 2 =)4byteになるので、
データ長として1837byteを指定しても
実際には1サンプル単位でしか扱われないため
実際は
・ori_len = frame_len - frame_len % sample_len
= 1837 - (1837 % 4) = 1836
となり、1836byte分しか再生されないことになります。
これでは
期待していたデータ長よりも短くなってしまっているので、
本来のテンポよりも速くなってしまいます。
また、データとして出力する際も
再生結果とデータの出力結果が(僅かながら)異なるもの
になってしまいます。
そこで、
毎フレーム行う波形書き込み処理に
少し細工をしてやります。
具体的には
1サンプル単位ではあぶれてしまう分を
毎フレーム蓄積してやり、
蓄積分が1サンプル以上になるようであれば、
そのフレームのフレーム長を1byte分だけ延ばしてやる。
という極めて単純な方法でOKです。
実際には
// 初期化・テンポ設定時に
// サンプル単位であぶれる分を計算して保持しておく。
int ex_sample = frame_len % sample_len;// 蓄積しないといけないので、
// 計算用の変数を別個に用意
int ex_data = 0;
// 1フレーム分の波形を書き込む処理
void WriteWaveBuf( ... )
{
// フレーム長
// sample_lenで割り切れる数値に調整しておく
long frame_len = buf_len;
...
// サンプル単位であぶれる分を蓄積
ex_data += ex_sample;// サンプル長以上だったら
// 1サンプル分長くする
if( ex_data >= sample_len )
{
frame_len += sample_len;// 忘れずに延ばした分を減算
ex_data -= sample_len;
}// 一応、後でヘッダを再設定するのを忘れないこと(苦笑)
...
}
大体こんな感じになるんでしょうかね。
(いろいろと適当なので、どんな変数が必要かなのかだけ見てください…(汗))
一応、
・frame_len はsample_lenで割り切れる数値にしておくこと
・1サンプル分加算するときに、蓄積分を減算すること
・バッファ長が変更されたらヘッダの更新が必要なこと
この辺は忘れやすいので注意しましょう。
- 1 - 3 さらに高い精度でデータ長を得る (byte単位以下、ms単位、またそれ以下の精度の取得)
と、ここまでやれば
ほとんど気にならない程度の精度が得られているかもしれません。
しかし、まだここまででは「1byte単位」の精度しかありません。
これより精度を上げるためには
フレーム長を再度「時間単位」で考える必要があります。
何をするかというと、
「frame_lenを時間単位に変換したもの」と
「frame_time」を比較してやります。
frame_lenとframe_timeが同値になることは
(ないとは言いませんがほとんど)ないので、
これの差を先程と同じ容量で蓄積してやり、
1サンプル(もしくは1byte)分の時間になるようであれば
やはり同じようにframe_lenを延ばしてやればいいわけです。
例えば、先程と同じく
「timebase = 48, tempo = 120」の場合は
・frame_time = 10.416666...(ms)
・frame_len = 1837(byte)
・one_ms_len = 1000 / sampling_rate
= 1000 / 44100 = 0.0226757...
・one_byte_len = one_ms_len / sample_len
= 0.0056893424...
・frame_data_time = frame_len / 1000 / sample_len / sampling_rate
= 10.4138321995...
・ex_ms = frame_time - frame_data_time
= 0.002834467120...
となるので、
このex_msを毎フレーム蓄積してやればいいわけです。
実際には(先程とほとんど変わりませんが)
大体こんな感じで。
// 1byteであぶれる分
// 面倒なので計算は省略(苦笑)
double ex_ms;// 計算用
double ex_ms_data;
// 1フレーム分の波形を書き込む処理
void WriteWaveBuf( ... )
{
...
// あぶれる分を蓄積
// 一応、上記の分とは別個に行うこと
ex_ms_data += ex_ms;// 1byte以上になるようなら加算
if( ex_ms_data >= one_byte_len )
{
// この「ex_data」は
// 上記と同じものだと面倒にならない
ex_data ++;// 忘れずに減算
ex_ms_data -= one_byte_len;
}
...
}
コメントの通り
「ex_data」は共通で扱った方がいろいろと都合がいいです。
(一応、上記と同様…(汗))
- 2 - 0 ちょっとした諸注意 (キレイに鳴らすための注意点)
というわけで
ここまでやってようやっと
ms以下とかμsとかの精度で再生できるようになります。
ただ、まだ二点ほど注意すべき点があります。
・1フレームで延ばすのは「1サンプルまで」にすること
・ヘッダの更新のタイミング・更新そのものを忘れないように注意すること
この二点です。
=== フレーム長
余分なサンプルを蓄積させていると、
時々2サンプル以上のデータが蓄積することがあります。
そんなときでも慌てず騒がず、1サンプルずつ処理しましょう
と、そういうことです。
もし、これを気にせずに
蓄積してある分を全て延長させるような真似をすると、
(一瞬、周波数が変わってしまい)波形が乱れてしまうことがあります。
特に矩形波等だと、かなり顕著に乱れが現れるので、注意が必要です。
(後、ここからは余談になってしまいますが、
2サンプル以上溜まっているのに、
1サンプルずつで大丈夫なのか?
という話です。
結論からいうと、大丈夫なんですが、
何でかというと、
・ex_len = ex_sample + ex_ms_len
・ex_sample < sample_len
・ex_sampleは整数
・ex_ms_len < 1
の4つの式が成り立たないといけないので、
どうあがいても
・ex_len < sample_len
となります。
なので、
毎フレーム1サンプルずつ処理していけば
十分に間に合うということになります。
…まぁ、いちいち式なんぞ持ち出さなくてもよくよく考えたら当たり前なんですが…(苦笑))
=== ヘッダの更新
たとえバッファ長を上手く計算して調整したとしても、
ヘッダを更新してやらないと、実際の再生には反映されません。
…まぁ、早い話忘れやすいので気をつけろよと…それだけですが(汗)
後、サウンドボードによっては
ヘッダの解放が異様に重いものがあるようなので、
ヘッダの更新には多少注意した方がいいかもしれません。
- 3 - 0 あとがき
というわけで、
大体書き残しておくべきことは大体書いた…かな?
一応、今回は
精度面と扱いやすさから浮動小数のdouble型を使っていますが
もしかしたら固定小数型を使った方が
より高速になったり、使いやすくなったりするかもしれませんね。
(…どちらにしても小数部は10bit分位要るかな?μs単位で欲しい場合は。 > 固定小数)
それでは、
この記事が何かの役に立てばいいなぁとか思いつつ
サウンドプログラミングの繁栄を願いつつ?
この辺で終了させて頂きます。