DirectSound1

DirectSoundは今まで学んできたことのまとめと言ってもいいかもしれません。音を鳴らすにはタイマー、DMA、割り込み機能を使って実現します。ここではガチガチに決まったルールに従って、サンプルコードを追いかける形になると思います。まず前座として、音について話をさせてください。

音とは

普段何気なく聞いている音楽。その記録媒体の1つにコンパクトディスク(CD)があります。円盤の1秒は44100分割されたデータであり、音の高さは2バイトです。これをwavファイルフォーマットに置き換えると以下のようになります。

44100(Hz) * 2バイト(16bit) * 2(ステレオ)* 1(秒) + 44(ヘッダ) = 176,444バイト
1.png

wavファイルの中身はヘッダ部とデータ部に分かれています。ヘッダは0x00-0x43までの44バイト。残りのデータは0x0000で埋まり、ファイルサイズは176,444バイトとなっています。音の高さ2バイトの中身は、16bit signed(-32768 ~ +32767, 無音は0)です。ようするに、音とはわかりやすい素直なデータであって何か不思議なことをしているわけではない、ということです。まずここを話のとっかかりにさせてもらいます。

440Hzのラ音

次に音をプログラムで作ってみましょう。以下にpythonのソースコードを載せますが、読まなくても結構です。

import numpy as np
import wave
import struct
from matplotlib import pylab as plt

fname = '1sec_440Hz.wav'

fs  = 44100
f   = 440
sec = 1
A   = 32767
samples = sec * fs

t = np.linspace(0, sec, samples)
s = A * np.sin(2 * np.pi * f * t)
s = s.astype(np.int16)

data = struct.pack("h" * samples , *s)

wf = wave.open(fname, 'w')
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(fs)
wf.writeframes(data)
wf.close()

plt.plot(s[0:441])
plt.show()

プログラムは440Hzのドレミファでいうラ音を1秒間モノラル出力したものです。440Hzとは1秒間の振動数を表しています。以下のグラフの横軸は0〜441、つまり全体の100分の1をグラフ化したものです。縦軸は(繰り返しの説明になりますが)16bit signed(-32768 ~ +32767, 無音は0)となっています。先ほどは音データの入れ物の話をしましたが、ここでは音の中身についてを見ています。githubのtoolフォルダにwavファイルを入れてありますので聞いたことない方はぜひ視聴してください。

2.png

青い線のとおり、16bit singedの数字が個々に格納されています。視覚化すると大変わかりやすいですね。

ドなら262Hz、レなら294Hz、ミなら330Hzといった値になります。ドレミを比較してみると振動幅が短くなっているのがわかると思います。音のカタマリの音楽とは、このようなデータで構成されているものだと思ってください。

3.png

GBAで鳴らしてみる

GBAはモノラル、符号付き8bit signed(-128 ~ +128, 無音は0)です。またGBATEKでは次のようにアドバイスしています。

適当翻訳すると「GBAは32.768kHzで音が鳴るから1秒間を32768Hz, 16384Hz, 8192Hz」のどれかにした方がいいぜ、といっています。例でいえば、画像ファイルのアスペクト比を無視して縮小をするんじゃないぞ、と忠告してもらっている感じです。

加工方法

さきほどのサンプルコードを横幅1秒間44100→16384にして、縦幅signed 16bit→8bitに変更します。

import numpy as np
import wave
import struct
from matplotlib import pylab as plt


list_file = ['1_c3.wav', '2_d3.wav', '3_e3.wav', '4_f3.wav', '5_g3.wav', '6_a3.wav', '7_b3.wav', '8_c4.wav']
list_hz = [262, 294, 330, 349, 392, 440, 494, 523]

fs  = 44100
sec = 1
A   = 32767
samples = sec * fs

for i in range(len(list_hz)):
	# Wav用
	t = np.linspace(0, sec, samples)
	s = A * np.sin(2 * np.pi * list_hz[i] * t)
	s = s.astype(np.int16)

	data = struct.pack("h" * samples , *s)

	wf = wave.open(list_file[i], 'w')
	wf.setnchannels(1)
	wf.setsampwidth(2)
	wf.setframerate(fs)
	wf.writeframes(data)
	wf.close()
 
	plt.plot(s[0:441])
	plt.show()


	# GBA用
	fs_gba = 16384
	samples_gba = sec * fs_gba

	t = np.linspace(0, sec, fs_gba)
	s = A * np.sin(2 * np.pi * list_hz[i] * t)
	s = s.astype(np.int16)

	for j in range(len(s)):
		s[j] = s[j] >> 8

	data = struct.pack("b" * samples_gba , *s)

	wf = open(list_file[i][:-4]+'.bin', 'wb')
	wf.write(data)
	wf.close()

	plt.plot(s[0:163])
	plt.show()
4.png

グラフの縦軸、横軸の値が変わっていますね。波形はおんなじです。(^^;

wav(windows用)bin(GBA用)
ファイルサイズ88,200+ヘッダ分44バイト16,384バイト

GBAのソースコード

最後にGBAで鳴らす方法です。ここはガチガチに決まった作法的で固まっています。

BGMはTimer0, DMA1、SEはTimer1, DMA2を担当しています。まずSndInit関数についてです。

EWRAM_CODE void SndInit()
{
	_Memset(&Snd, 0x00, sizeof(ST_SND) * 2);

	REG_TM0CNT_L   = 0x10000 - SND_FRAQ;
	REG_TM1CNT_L   = 0x10000 - SND_FRAQ;

	REG_SOUNDCNT_X = SNDSTAT_ENABLE;
	REG_SOUNDCNT_L = 0;
	REG_SOUNDCNT_H = SNDA_RESET_FIFO | SNDB_RESET_FIFO | SNDA_VOL_100 | SNDB_VOL_100 | SNDA_TIMER0 | SNDB_TIMER1;
}

タイマー割り込みを行う為の値をセットしています。1秒間のCPUクロック数16,777,216を、1秒間の音データ数16384で割り算します。

16777216 / 16384 = 1024

1024という数字が出てきました。言い方を変えると1024クロック毎にタイマー割り込みをさせ(REG_TMxCNT_Lを溢れさせ)、1バイトづつ送ると1秒後に16384バイト送ったことになります。次にサウンドレジスタについてです。

SOUNDCNT_X

4000084h - SOUNDCNT_X (NR52) - Sound on/off (R/W)

Bits 0-3 are automatically set when starting sound output, and are automatically cleared when a sound ends.
(Ie. when the length expires, as far as length is enabled. The bits are NOT reset when an v olume envelope ends.)

  Bit        Expl.
  0     R    Sound 1 ON flag (Read Only)
  1     R    Sound 2 ON flag (Read Only)
  2     R    Sound 3 ON flag (Read Only)
  3     R    Sound 4 ON flag (Read Only)
  4-6   -    Not used
  7     R/W  PSG/FIFO Master Enable (0=Disable, 1=Enable) (Read/Write)
  8-31  -    Not used

While Bit 7 is cleared, both PSG and FIFO sounds are disabled,
and all PSG registers at 4000060h..4000081h are reset to zero
(and must be re-initialized after re-enabling sound).

However, registers 4 000082h and 4000088h are kept read/write-able
(of which, 4000082h has no function when sound is off, whilst 4000088h does work even when sound is off).
REG_SOUNDCNT_X = SNDSTAT_ENABLE;

FIFOを使うためにEnableにしています。FIFOとは音データを扱うメモリ領域です。DMAやTimerが連動してデータを渡します。

SOUNDCNT_L

4000080h - SOUNDCNT_L (NR50, NR51) - Channel L/R Volume/Enable (R/W)

  Bit        Expl.
  0-2   R/W  Sound 1-4 Master Volume RIGHT (0-7)
  3     -    Not used
  4-6   R/W  Sound 1-4 Master Volume LEFT (0-7)
  7     -    Not used
  8-11  R/W  Sound 1-4 Enable Flags RIGHT (each Bit 8-11, 0=Disable, 1=Enable)
  12-15 R/W  Sound 1-4 Enable Flags LEFT (each Bit 12-15, 0=Disable, 1=Enable)
REG_SOUNDCNT_L = 0;

初期化の時点では鳴らないようにしています。

SOUNDCNT_H

4000082h - SOUNDCNT_H (GBA only) - DMA Sound Control/Mixing (R/W)

  Bit        Expl.
  0-1   R/W  Sound # 1-4 Volume   (0=25%, 1=50%, 2=100%, 3=Prohibited)
  2     R/W  DMA Sound A Volume   (0=50%, 1=100%)
  3     R/W  DMA Sound B Volume   (0=50%, 1=100%)
  4-7   -    Not used
  8     R/W  DMA Sound A Enable RIGHT (0=Disable, 1=Enable)
  9     R/W  DMA Sound A Enable LEFT  (0=Disable, 1=Enable)
  10    R/W  DMA Sound A Timer Select (0=Timer 0, 1=Timer 1)
  11    W?   DMA Sound A Reset FIFO   (1=Reset)
  12    R/W  DMA Sound B Enable RIGHT (0=Disable, 1=Enable)
  13    R/W  DMA Sound B Enable LEFT  (0=Disable, 1=Enable)
  14    R/W  DMA Sound B Timer Select (0=Timer 0, 1=Timer 1)
  15    W?   DMA Sound B Reset FIFO   (1=Reset)
REG_SOUNDCNT_H = SNDA_RESET_FIFO | SNDB_RESET_FIFO | SNDA_VOL_100 | SNDB_VOL_100 | SNDA_TIMER0 | SNDB_TIMER1;

define定義どおりです。Sound A, BにTimerなどの設定をしています。次に再生部分についてです。SndIntrBgmやSndIntrSeはvblank毎に呼び出されます。そして音を鳴らす最初は、SndIntrBgmStart、SndIntrSeStartを呼び出します。

IWRAM_CODE void SndIntrBgmStart(void)
{
	REG_TM0CNT_H = 0;
	REG_DMA1CNT  = 0;

	DMA1COPY(Snd[0].data, &REG_FIFO_A, DMA_SPECIAL | DMA32 | DMA_REPEAT | DMA_SRC_INC | DMA_DST_FIXED);
	REG_TM0CNT_H    = TIMER_START;
	REG_SOUNDCNT_H |= (SNDA_R_ENABLE | SNDA_L_ENABLE | SNDA_RESET_FIFO);

	Snd[0].cnt = Snd[0].frameCnt;
}

ここもガチガチの作法というか、他に書きようがない部分と思われます。DMA1では転送元を音楽データ、転送先をFIFOメモリ領域に指定します。

FIFO_A

40000A0h - FIFO_A_L - Sound A FIFO, Data 0 and Data 1 (W)
40000A2h - FIFO_A_H - Sound A FIFO, Data 2 and Data 3 (W)

These two registers may receive 32bit (4 bytes) of audio data
(Data 0-3, Data 0 being located in least significant byte which is replayed first).

Internally, the capacity of the FIFO is 8 x 32bit (32 bytes),
allowing to buffer a small amount of samples. As the name says (First In First Out),
oldest data is replayed first.

DirectSoundを使うということは、タイマーやDMAの使用予約が決まります。自作プログラミングで使えなくなるので注意してください。なお終了方法に関してはVblankをカウンタにして、0になったら終了するかリスタートするかという方法をとっています。

動作画面

おまけでカエルの歌を演奏できるようにしてみました。(^^;

5.png

履歴


トップ   一覧 検索 最終更新   ヘルプ   最終更新のRSS