DirectSoundは今まで学んできたことのまとめと言ってもいいかもしれません。音を鳴らすにはタイマー、DMA、割り込み機能を使って実現します。ここではガチガチに決まったルールに従って、サンプルコードを追いかける形になると思います。まず前座として、音について話をさせてください。
普段何気なく聞いている音楽。その記録媒体の1つにコンパクトディスク(CD)があります。円盤の1秒は44100分割されたデータであり、音の高さは2バイトです。これをwavファイルフォーマットに置き換えると以下のようになります。
44100(Hz) * 2バイト(16bit) * 2(ステレオ)* 1(秒) + 44(ヘッダ) = 176,444バイト
wavファイルの中身はヘッダ部とデータ部に分かれています。ヘッダは0x00-0x43までの44バイト。残りのデータは0x0000で埋まり、ファイルサイズは176,444バイトとなっています。音の高さ2バイトの中身は、16bit signed(-32768 ~ +32767, 無音は0)です。ようするに、音とはわかりやすい素直なデータであって何か不思議なことをしているわけではない、ということです。まずここを話のとっかかりにさせてもらいます。
次に音をプログラムで作ってみましょう。以下に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ファイルを入れてありますので聞いたことない方はぜひ視聴してください。
青い線のとおり、16bit singedの数字が個々に格納されています。視覚化すると大変わかりやすいですね。
ドレミ記譜法 | ABC記譜法 | 周波数 |
ド | C | 262 |
レ | D | 294 |
ミ | E | 330 |
ファ | F | 349 |
ソ | G | 392 |
ラ | A | 440 |
シ | B | 494 |
ド | C | 523 |
ドなら262Hz、レなら294Hz、ミなら330Hzといった値になります。ドレミを比較してみると振動幅が短くなっているのがわかると思います。音のカタマリの音楽とは、このようなデータで構成されているものだと思ってください。
GBAはモノラル、符号付き8bit signed(-128 ~ +128, 無音は0)です。またGBATEKでは次のようにアドバイスしています。
The GBA hardware does internally re-sample all sound output to 32.768kHz (default SOUNDBIAS setting). It'd thus do not make much sense to use higher DMA/Timer rates. Best re-sampling accuracy can be gained by using DMA/Timer rates of 32.768kHz, 16.384kHz, or 8.192kHz (ie. fragments of the physical output rate).
適当翻訳すると「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()
グラフの縦軸、横軸の値が変わっていますね。波形はおんなじです。(^^;
wav(windows用) | bin(GBA用) | |
ファイルサイズ | 88,200+ヘッダ分44バイト | 16,384バイト |
最後にGBAで鳴らす方法です。ここはガチガチに決まった作法的で固まっています。
#ifndef __SND_H__ #define __SND_H__ #ifdef __cplusplus extern "C" { #endif #include "lib/gba.h" //--------------------------------------------------------------------------- #define SND_CPU_CLOCK (16 * 1024 * 1024) #define SND_AUDIO_RATE 16384 #define SND_FRAQ (SND_CPU_CLOCK / SND_AUDIO_RATE) enum { SND_ID_BGM = 0, SND_ID_SE, }; enum { SND_ACT_DONOTHING = 0, SND_ACT_START, SND_ACT_STOP, SND_ACT_PLAY, }; //--------------------------------------------------------------------------- typedef struct { u32 act; s32 cnt; u8* data; u32 size; s32 frameCnt; bool isLoop; } ST_SND; //--------------------------------------------------------------------------- EWRAM_CODE void SndInit(void); IWRAM_CODE void SndPlay(u32 id, u8* data, u32 size, s32 adjust, bool isLoop); IWRAM_CODE void SndPlayBgm(u8* data, u32 size, s32 adjust, bool isLoop); IWRAM_CODE void SndPlaySe(u8* data, u32 size, s32 adjust, bool isLoop); IWRAM_CODE void SndStopBgm(void); IWRAM_CODE void SndStopSe(void); IWRAM_CODE bool SndIsPlayBgm(void); IWRAM_CODE bool SndIsPlaySe(void); IWRAM_CODE void SndIntrBgm(void); IWRAM_CODE void SndIntrSe(void); IWRAM_CODE void SndIntrBgmStart(void); IWRAM_CODE void SndIntrBgmStop(void); IWRAM_CODE void SndIntrSeStart(void); IWRAM_CODE void SndIntrSeStop(void); #ifdef __cplusplus } #endif #endif
#include "snd.arm.h" // BGM Timer0, DMA1 // SE Timer1, DMA2 //--------------------------------------------------------------------------- ST_SND Snd[2]; //--------------------------------------------------------------------------- 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 | DSOUNDCTRL_ATIMER(0) | DSOUNDCTRL_BTIMER(1); } //--------------------------------------------------------------------------- IWRAM_CODE void SndPlay(u32 id, u8* data, u32 size, s32 adjust, bool isLoop) { ST_SND* p = &Snd[id]; p->act = SND_ACT_START; p->cnt = 0; p->data = data; p->size = size; p->frameCnt = (size * 60) / SND_AUDIO_RATE + adjust; p->isLoop = isLoop; } //--------------------------------------------------------------------------- IWRAM_CODE void SndPlayBgm(u8* data, u32 size, s32 adjust, bool isLoop) { SndPlay(SND_ID_BGM, data, size, adjust, isLoop); } //--------------------------------------------------------------------------- IWRAM_CODE void SndPlaySe(u8* data, u32 size, s32 adjust, bool isLoop) { SndPlay(SND_ID_SE, data, size, adjust, isLoop); } //--------------------------------------------------------------------------- IWRAM_CODE void SndStopBgm(void) { Snd[SND_ID_BGM].act = SND_ACT_STOP; } //--------------------------------------------------------------------------- IWRAM_CODE void SndStopSe(void) { Snd[SND_ID_SE].act = SND_ACT_STOP; } //--------------------------------------------------------------------------- IWRAM_CODE bool SndIsPlayBgm(void) { return (Snd[SND_ID_BGM].act == SND_ACT_PLAY) ? TRUE : FALSE; } //--------------------------------------------------------------------------- IWRAM_CODE bool SndIsPlaySe(void) { return (Snd[SND_ID_SE].act == SND_ACT_PLAY) ? TRUE : FALSE; } //--------------------------------------------------------------------------- IWRAM_CODE void SndIntrBgm(void) { ST_SND* p = &Snd[0]; switch(p->act) { case SND_ACT_DONOTHING: return; case SND_ACT_START: start: SndIntrBgmStart(); p->act = SND_ACT_PLAY; return; case SND_ACT_STOP: stop: SndIntrBgmStop(); p->act = SND_ACT_DONOTHING; return; case SND_ACT_PLAY: p->cnt--; if(p->cnt <= 0) { if(p->isLoop == TRUE) { goto start; } else { goto stop; } } return; default: SystemError("[Err] SndIntrBgm"); break; } } //--------------------------------------------------------------------------- IWRAM_CODE void SndIntrSe(void) { ST_SND* p = &Snd[1]; switch(p->act) { case SND_ACT_DONOTHING: return; case SND_ACT_START: start: SndIntrSeStart(); p->act = SND_ACT_PLAY; return; case SND_ACT_STOP: stop: SndIntrSeStop(); p->act = SND_ACT_DONOTHING; return; case SND_ACT_PLAY: p->cnt--; if(p->cnt <= 0) { if(p->isLoop == TRUE) { goto start; } else { goto stop; } } return; default: SystemError("[Err] SndIntrSe"); break; } } //--------------------------------------------------------------------------- IWRAM_CODE void SndIntrBgmStart(void) { REG_TM0CNT_H = 0; REG_DMA1CNT = 0; DMA1COPY(Snd[0].data, ®_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; } //--------------------------------------------------------------------------- IWRAM_CODE void SndIntrBgmStop(void) { REG_SOUNDCNT_H &= ~(SNDA_R_ENABLE | SNDA_L_ENABLE | SNDA_RESET_FIFO); REG_TM1CNT_H = 0; REG_DMA1CNT = 0; Snd[0].cnt = 0; } //--------------------------------------------------------------------------- IWRAM_CODE void SndIntrSeStart(void) { REG_TM1CNT_H = 0; REG_DMA2CNT = 0; DMA2COPY(Snd[1].data, ®_FIFO_B, DMA_SPECIAL | DMA32 | DMA_REPEAT | DMA_SRC_INC | DMA_DST_FIXED); REG_TM1CNT_H = TIMER_START; REG_SOUNDCNT_H |= (SNDB_R_ENABLE | SNDB_L_ENABLE | SNDB_RESET_FIFO); Snd[1].cnt = Snd[1].frameCnt; } //--------------------------------------------------------------------------- IWRAM_CODE void SndIntrSeStop(void) { REG_SOUNDCNT_H &= ~(SNDB_R_ENABLE | SNDB_L_ENABLE | SNDB_RESET_FIFO); REG_TM1CNT_H = 0; REG_DMA2CNT = 0; Snd[1].cnt = 0; }
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バイト送ったことになります。次にサウンドレジスタについてです。
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が連動してデータを渡します。
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;
初期化の時点では鳴らないようにしています。
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, ®_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メモリ領域に指定します。
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になったら終了するかリスタートするかという方法をとっています。
おまけでカエルの歌を演奏できるようにしてみました。(^^;