2007年06月06日

VCOの設計とテスト

正弦波と三角波、鋸波を生成するVCOが一応できた。


コードは次のようなもの。


prog_char sintab[64] = {
0, 3, 6, 9, 12, 15, 18, 21,
24, 27, 30, 33, 36, 39, 42, 45,
48, 51, 54, 57, 59, 62, 65, 67,
70, 73, 75, 78, 80, 82, 85, 87,
89, 91, 94, 96, 98, 100, 102, 103,
105, 107, 108, 110, 112, 113, 114, 116,
117, 118, 119, 120, 121, 122, 123, 123,
124, 125, 125, 126, 126, 126, 126, 126,
};

struct VCO {
uint16_t wave_pos;
int16_t wave_inc;
int8_t lfo_inc;
uint8_t wave_type;
} vco;

static void generate_snd() {
uint8_t pos;
int8_t rev = 0;
int8_t v;
vco.wave_pos += vco.wave_inc + vco.lfo_inc;
pos = vco.wave_pos >> 8;
if (pos >= 128) {
rev = 1;
pos = 255 - pos;
}
if (vco.wave_type == WAVE_SAW) {
v = 127 - pos;
} else {
if (pos >= 64) {
pos = 127 - pos;
}
if (vco.wave_type == WAVE_TRI) {
v = pos*2;
} else
v = __LPM(sintab+pos);
}
if (rev) v = -v;
sndout(v);
}


サンプリングレートで、この関数を call すると、そのときの出力を計算して sndout() で出力する。アルゴリズムは、DDS(ダイレクトデジタルシンサイザ)。

メインループはこんなかんじ↓。can_sndout() で バッファーが空いていれば、generate_snd() で次のデータを作ってバッファーに詰め込む。

for (;;) {
if (can_sndout()) {
generate_snd();
continue;
}
if (usbcdc_can_getc(1)) {
c = usbcdc_getc();
change_note(c);
}
}


周波数 HZ の波形を出力したい場合は、

vco.wave_inc = HZ * 256 * 256 / SAMPLE_RATE;

となるようにすれば良い。
ちなみに、vco.lfo_inc は、LFO で周波数を揺らすためのものだが LFOはまだできていない。

もちろん、音階に対応した vco.wave_inc の値はあらかじめ作っておく。

prog_uint16_t note_tab[12] = {
8572944/ SAMPLE_RATE,
9082717/ SAMPLE_RATE,
9622802/ SAMPLE_RATE,
10195003/ SAMPLE_RATE,
10801229/ SAMPLE_RATE,
11443502/ SAMPLE_RATE,
12123967/ SAMPLE_RATE,
12844895/ SAMPLE_RATE,
13608690/ SAMPLE_RATE,
14417904/ SAMPLE_RATE, // A3 220 Hz
15275236/ SAMPLE_RATE,
16183547/ SAMPLE_RATE,
};


ちなみにこの値は、220 Hz * 256 * 256 をもとに、2の12乗根 1.059463 を乗除して作ったもの。uint16_t に収まる必要があるので、表現できる最大の周波数は、SAMPLE_RATE ということになる。

ずいぶんはしょってしまったが、本題はこれから。はたして、これで進めてしまってよいのだろうか? グラフ化した波形(先頭のグラフ)はそれなりにまともに見えるが、いったいどんな音になるのだろう -- 確かめてから先に進みたい。

Linuxのサウンド出力

というわけで、Linux でどのようにサウンドを出力するのか調べて作ってみたのが次のコード(説明のためにエラー処理はすべて除いた)。

int sndout_fd;
int audio_buf_size;

static void sndout_init() {
int arg;
struct audio_buf_info info;

sndout_fd = open("/dev/dsp",O_RDWR);
arg = 16;
ioctl(sndout_fd, SOUND_PCM_WRITE_BITS, &arg);
arg = 2;
ioctl(sndout_fd, SOUND_PCM_WRITE_CHANNELS, &arg);
arg = SAMPLE_RATE;
ioctl(sndout_fd, SOUND_PCM_WRITE_RATE, &arg);
arg = 0x0008000c; // fragstotal 8 fragsize 4096
ioctl(sndout_fd, SOUND_PCM_SETFRAGMENT, &arg);
ioctl(sndout_fd, SOUND_PCM_GETOSPACE, &info);
audio_buf_size = info.bytes;
}

static int can_sndout() {
struct audio_buf_info info;
r = ioctl(sndout_fd, SOUND_PCM_GETOSPACE, &info);
if (audio_buf_size - info.bytes >= info.fragsize * 2) {
return 0;
}
return 1;
}

static void sndout(int8_t d) {
int16_t buf[2];
buf[0] = (int16_t)d <<8;
buf[1] = (int16_t)d <<8;
write(sndout_fd, buf, 4);

}


ポイントは、fragsizeを小さくした上で、バッファーが一杯残っている段階で FULL と認識して詰め込みすぎないようにすること。そうしないと、パラメータを変更したときの追従性が悪くなってしまう。
参考にしたのは、
Linux Sound programming with OSS API(リアルタイム性を考慮する)
Linux Sound programming with OSS API(/dev/dsp の機能を調べる)

追記:
私の環境では、どうも 2ch/16 BitParSample (S16LE) 以外設定できないようで、4倍の周波数(32K Hz)になってしまっていました。なので、S16LE 用のコードに変更しました。


Windowsのサウンド出力


#include <mmsystem.h>

#define WAVEBUF_SIZE 1024
#define NR_WAVEHDR 8

HWAVEOUT HWaveOut = NULL;
uint8_t *WaveBuf;
WAVEHDR WaveHdr[NR_WAVEHDR];
int cur_wavehdr = 0;
int cur_wavebuf_len = 0;
WAVEFORMATEX waveFmt;

static void sndout_init() {
int r,i;
waveFmt.wFormatTag = (WORD)0x0001; // PCM
waveFmt.nChannels = 1;
waveFmt.nSamplesPerSec = SAMPLE_RATE;
waveFmt.nAvgBytesPerSec = SAMPLE_RATE*1;
waveFmt.nBlockAlign = 1;
waveFmt.wBitsPerSample = 8;
waveFmt.cbSize = (WORD)0;

WaveBuf = GlobalAlloc(GPTR, NR_WAVEHDR * WAVEBUF_SIZE);
memset(WaveHdr, 0, sizeof(WAVEHDR) * NR_WAVEHDR);
for (i=0; i<NR_WAVEHDR; i++) {
WaveHdr[i].lpData = WaveBuf+ i * WAVEBUF_SIZE;
WaveHdr[i].dwBufferLength = WAVEBUF_SIZE;
}

waveOutOpen(&HWaveOut, WAVE_MAPPER, &waveFmt, 0, 0, CALLBACK_NULL);
for (i=0; i<NR_WAVEHDR; i++) {
waveOutPrepareHeader(HWaveOut,WaveHdr + i, sizeof(WAVEHDR));
}
}

static void sndout(int8_t d) {
int next;

next = cur_wavehdr + 1;
if (next >= NR_WAVEHDR) next -= NR_WAVEHDR;
if (cur_wavebuf_len >= WAVEBUF_SIZE) {
while (!(WaveHdr[next].dwFlags & WHDR_DONE)
&& (WaveHdr[next].dwFlags != WHDR_PREPARED)) {
fprintf(stderr,"wave buffer overflow\n");
exit(1);
}
waveOutWrite(HWaveOut, WaveHdr + cur_wavehdr, sizeof(WAVEHDR));
cur_wavehdr = next;
cur_wavebuf_len = 0;
}
WaveHdr[cur_wavehdr].lpData[cur_wavebuf_len++] = d+128;
}

static int can_sndout() {
int prev;

prev = cur_wavehdr - 3;
if (prev < 0) prev += NR_WAVEHDR;
if (WaveHdr[prev].dwFlags == WHDR_PREPARED) return 1;
if (WaveHdr[prev].dwFlags & WHDR_DONE) return 1;
return 0;
}


Windows の場合は、自分で fragment を作らないといけないので若干面倒。あと、fragment の数とか サイズ、詰め込みすぎないようにするところとか、Linux 版にあわせてみた。

Windows版で参考にしたのは、oggdecのソースコードとかportaudioとか
追記:
バグがありました。nBlockAlignには、waveFmt.nChannels * waveFmt.wBitsPerSample/8 (=1) を入れるべきで、あと、8 BitPerSample では、値が 0 〜 255 なので +128 してやる必要があります。(参考:WAVEデータの作成と再生


視聴してみて

聞いてみたらそれなりに聞こえたので、8bit から拡張するとか補完を入れて精度を上げるとかあまり考えずにゴリゴリいきたいと思う。そういうことはできてから考えれば良さそうだ。

おわりに

なんかコードばっかりのページになってしまった。ほとんどおまじないみたいなものなので、解説も微妙に書きにくい。もしもっと知りたいなら、ここに出てきたキーワードでググッてみてほしい。

ちなみに、実行可能なコードのソース ddstest1.cも置いておくので、どんな音になるか興味がある人は動かしてみてほしい。(注意: ddstest1 はバグっています。ddstest2a.cが機能拡張+バグ修正版なのですが、(キー入力の問題で)MinGWの環境ではまともに動きません。)
posted by すz at 23:07| Comment(2) | TrackBack(0) | I2CSND
この記事へのコメント
まだ LFO はできていないとのことですが、このインプリメントでは
vco.lfo_inc は、発生する音階によって変化させる必要があり、具体的には

vco.lfo_inc = vco.wave_inc * lfo_modulation_depth * lfo_value;

みたいな形になると思うのですが、その辺は考慮されているのでしょうか?
Posted by pcm1723 at 2007年06月07日 02:09
コメントありがとうございます。FM音源を自作されているのですね。FM音源的な使い方*も*できることを知らなかったので、今のもの ddstest2.c は、サンプリング周波数の 1/16の頻度で上のような計算をするようにしています。音作りは全然経験がないので、またご教授ください。
Posted by すz at 2007年06月07日 19:18
コメントを書く
お名前: [必須入力]

メールアドレス: [必須入力]

ホームページアドレス:

コメント: [必須入力]

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
この記事へのトラックバックURL
http://blog.sakura.ne.jp/tb/30760394
※ブログオーナーが承認したトラックバックのみ表示されます。

この記事へのトラックバック