どうも、GAIさんです。
今回はなんか、音楽の再生とやらをやっていきます。
参考にする動画はこちら
https://www.nicovideo.jp/watch/sm25525464
まず、動画内の最初の処理はこちら
.inesprg 1
.ineschr 0
.inesmir 0
.inesmap 0
.bank 0
.org $8000
Start:
lda #%10000000
sta $2000
lda #0
sta $2001
mainProcess:
inc $00
infinity:
jmp infinity
.bank 1
.org $FFFA
.dw mainProcess ;VBlank時のジャンプ先
.dw Start ;リセットボタン
.dw 0
動画の冒頭で説明している、このプロセスでやっていることは
・VBlankが発生したときにジャンプを有効にする設定(Start)
・VBlank時にmainProcessにジャンプする定義
・jmp命令で指定アドレスにジャンプ(infinityに飛んですぐjmpなので無限ループ)
順番に見ていく
VBlank時にジャンプを有効にする設定
lda #%10000000
sta $2000
lda #0
sta $2001
これをみると、$2000に%100000000を書き込んでるように見える。
・・・が、通常のメモリ書き込みとは違うらしい。
PPUについて
CPUじゃなくてPPU?
ってなるけど、これはPictureProcessingUnit。
ひらたく言えば、画像処理する場所。C(CPUはCentralP~。計算する場所)
んで、ファミコンカートリッジの仕組み上、CPUとPPUは別の場所に置いてあって、直接やりとりできない。
なので、CPUからPPUに指示を出すためのIOポートが設定されているらしい。
・・・ようは、直通ルートというか、本部から現場への連絡線みたいな(わかりづらい)
なぜPPUの話をしてるかというと、VBlankは画面の更新に関係することだから、画像を処理するPPUにそれを伝える必要があるってことね。
で、さっきのコードの sta \$2000とsta \$2001がVBlankの設定に関する、PPUへの直通ルート。
その結果、.dw mainProcessがちゃんと機能するようになると。
というか、VBlankでジャンプしないようにすることとかあるんや。
画像の更新を行わない場合とか?ファミコンの意味()
というわけで気になったのでAIにきいてみる。
- スキャンライン効果: ゲーム内で特殊な視覚効果を実現するために、スキャンラインごとに処理を行う必要がある場合があります。この場合、VBlankジャンプを使用せず、スキャンラインごとの処理を行うことで、意図的にちらつきを発生させ、独特の視覚効果を達成することができます。
- 高速処理: 一部のゲームでは、VBlank期間を待つことなく、できるだけ高速に処理を行う必要がある場合があります。この場合、VBlankジャンプを使用せず、常に処理を続けることで、ゲームの速度を向上させることができます。ただし、この手法は画面のちらつきや音声の乱れを引き起こす可能性があるため、慎重に使用する必要があります。
- 特殊なグラフィック表示: ゲーム内で特殊なグラフィック表示を行う場合、VBlankジャンプを使用しないことで、通常とは異なる画面更新タイミングを利用することができます。これにより、独自のグラフィック表示手法を実現することが可能です。
- デバッグ用途: ゲームの開発中、VBlankジャンプを一時的に無効にすることで、画面更新のタイミングを確認したり、処理速度を測定したりすることができます。これは、ゲームのパフォーマンス最適化やデバッグに役立ちます。
なるほど。
基本的にはVBlankは使うけど、画面の更新を待つ必要が無い場合とか画面の更新をコントロールしたいときにオフにするのね。
まあ、大前提使うということで覚えておこう。
$2000で設定できる項目
今回は%10000000を指定しているけど、これどんな意味なん?ってのが、以下。
ビット0-1: ネームテーブルの選択
00: \$2000(ネームテーブル0)
01: \$2400(ネームテーブル1)
10: \$2800(ネームテーブル2)
11: \$2C00(ネームテーブル3)
ビット2: VRAMインクリメントモード
0: 1バイトずつインクリメント
1: 32バイトずつインクリメント
ビット3: スプライトパターンテーブルアドレス
0: \$0000
1: \$1000
ビット4: 背景パターンテーブルアドレス
0: \$0000
1: \$1000
ビット5: スプライトサイズ
0: 8x8ピクセル
1: 8x16ピクセル
ビット6: PPUマスター/スレーブモード(読み取り専用)
0: マスターモード
1: スレーブモード
ビット7: VBlankインターラプト
0: 無効
1: 有効
$2001で設定できる項目
ビット0: グレースケール
0: カラー表示
1: グレースケール表示
ビット1: 背景の左端8ピクセルの表示
0: 左端8ピクセルを表示しない
1: 左端8ピクセルを表示する
ビット2: スプライトの左端8ピクセルの表示
0: 左端8ピクセルを表示しない
1: 左端8ピクセルを表示する
ビット3: 背景の表示
0: 背景を表示しない
1: 背景を表示する
ビット4: スプライトの表示
0: スプライトを表示しない
1: スプライトを表示する
ビット5: 背景の色強調
0: 背景の色強調を無効にする
1: 背景の色強調を有効にする
ビット6: スプライトの色強調
0: スプライトの色強調を無効にする
1: スプライトの色強調を有効にする
ビット7: 画面表示
0: 画面表示を無効にする
1: 画面表示を有効にする
・・・で?
まあ、今書かれてもわからんよ!って話。
GAIさんもそう。みんなそう。だから項目だけ。
つまり
Start:
lda #%10000000
sta $2000
lda #0
sta $2001
ここでやっていることは、
VBlankのインターラプトを有効にして、そのほかは全部無効にしてるってことだよ。
mainProcessのインクリクリクリクリクリメント
インクリメントです。
mainProcess:
inc $00 ;これ
.(ドット)がついてないから、これはCPUへの命令だね。覚えてる。
incは指定メモリ番地を1インクリメントする。インクリメントは、+1するってことだよ。
まあつまり、$00番地の中身を+1するってことだね。
ここでやってる理由は、ループされてるか確認したいからなので、それ以上の意味はないね。
動画の説明で、luascriptで~みたいなことを言っているから、FCEUXのエミュレータに変更。
これ
https://fceux.com/web/home.html
File>Lua>New Lua Script Windowで以下を読み込ませてRUN
while true do
gui.text(8,8,memory.readbyteunsigned(0x00));
emu.frameadvance();
end;
では、コンソール開いてnesasm_x64を実行
↓コピペ用
nesasm_x64 test.asm
無限ループして、255で0に戻った。
なるほどね。
さて、音を出すぞAPU
CPU、PPUに続き、APUがでてきました。
これはAudio Processing Unit。まあ、音を出す処理担当。
そしてPPUと同様にファミコンカセットの仕組み上、IOポートを使って指示。
物理の壁を高い。
画像関係が\$2000と\$2001に対し、音は\$4000~$4015とのこと。
APUさん音出しお願いしゃす!
動画の2:55あたり。
ここまで書いてもまだ3分いってないのか・・・
まずStartを以下に変更
Start:
lda #%10000000
sta $2000
lda #0
sta $2001
lda #%00000001
sta $4015
lda #%10111111
sta $4000
lda #%00000000
sta $4001
lda #126
sta $4002
lda #%00000000
sta $4003
なあにこれえ(ATM
これで実行すると、永遠にピーー音がなります。ラの音。
lda #%00000001
sta $4015
このパラメータで音声の設定をしているらしい。
PPUと同様に各ビットに割り当てがされている。
00000001の下5桁で音声チャンネルのONOFF設定をしてる。
ファミコンは5つの音声チャンネルをもっていて、それらのONOFFをしてるとのこと。
1:矩形波(\$4000~3)
2:矩形波(\$4004~7)
3:三角波(\$4008~B)
4:ノイズ(\$400C~F)
4:差分PCM(\$4010~13)
の5つ。なるほどね。DTMやってないとなんだかわからんね。
まあ、ファミコンっぽい音のパターン的な。
つまりここでは、矩形波1だけオンにしてる感じ。
$4000の内容
次は実際に音を出す命令
lda #%10111111
sta $4000
\$4000は矩形波1の一つだね。
そこに与えているパラメータは%10111111
これが何を意味しているかというと
上の桁から
10 : Duty比
1 : 音の再生時間
1 : 減衰無効
1111 : 減衰率
Duty比って何?
教えてAI先生
ファミコンの音源では、Duty比を変えることで、音色を変化させることができるんだ。
ファミコンの音源チップは、2A03(NTSC版)または2A07(PAL版)というチップが使われていて、これには4つの音源チャンネルがあるよ。そのうちの2つのチャンネル(矩形波チャンネル1と2)では、Duty比を設定できるんだ。
Duty比は、4つのパターンから選ぶことができて、それぞれ12.5%、25%、50%、75%に対応しているんだ。Duty比が小さいほど、音色は細い感じになり、大きいほど太い感じになるよ。
例えば、12.5%のDuty比だと、ピコピコしたような細い音になるんだ。一方、50%のDuty比だと、ブザーのような太い音になる。ゲームの BGMや効果音で、これらのDuty比を使い分けることで、様々な音色を表現できるんだよ。
プログラムの中では、音源チャンネルごとに設定されている音量/エンベロープのレジスタにDuty比のビットを設定することで、Duty比を変更するんだ。
このように、ファミコンの音源でDuty比を変えることで、同じ音程でも異なる音色を作り出すことができるから、ゲームの雰囲気に合わせて使い分けることが大切なんだよ。
めちゃめちゃわかった
ファミコンで設定できるDuty比は4種類
00 : 12.5%
01 : 25.0%
10 : 50.0%
11 : 75.0%
同じラの音でも音色が随分変わる。
音の再生時間($4003で指定)
1 : なりっぱなし
0 : 時間で止まる
再生時間は、各チャンネルの下位5ビットで指定。
1矩形波は\$4000~\$4003なので\$4003の中
lda #%00000000 ;ここの上5桁の00000
sta $4003
で、音の長さに1をいれたら何秒になるの?って計算はこれ。
音の長さ(秒) = ((音長カウンタの初期値 + 1) * 16 * 1/4) / 60
仕組み?そんなん知らなくてもいいんだよ()
教えてAI先生!
音長カウンタの初期値と実際のカウント値の関係は、以下の式で表されるんだ。
実際のカウント値 = (音長カウンタの初期値 + 1) * 16
例えば、音長カウンタの初期値に10を設定した場合、実際のカウント値は以下のように計算されるよ。
実際のカウント値 = (10 + 1) * 16 = 176
つまり、レジスタに10を設定すると、実際には176カウントの長さで音が鳴ることになるんだ。
音の長さを計算する式に当てはめると、以下のようになるよ。
音の長さ(秒) = ((音長カウンタの初期値 + 1) * 16 * 1/4) / 60
このように、レジスタに設定する音長カウンタの初期値は、実際のカウント値よりも少し小さい値になっているんだ。プログラムを書くときは、この関係を理解しておく必要があるよ。
ただし、音長カウンタの初期値が0の場合は特別で、カウント値は0になるんだ。つまり、音長カウンタの初期値を0にすると、音は鳴らなくなるから気をつけてね。
\$4003に%00001000をセットした場合、音長カウンタの初期値は8になるよ。先ほどの式を使って、実際のカウント値と音の長さを計算してみるね。
まず、実際のカウント値は、
実際のカウント値 = (音長カウンタの初期値 + 1) * 16
= (8 + 1) * 16
= 144
次に、音の長さを計算すると、
音の長さ(秒) = ((音長カウンタの初期値 + 1) * 16 * 1/4) / 60
= ((8 + 1) * 16 * 1/4) / 60
= 0.6秒 = 600ミリ秒
つまり、\$4003に%00001000をセットすると、音は0.6秒(600ミリ秒)の長さで鳴ることになるよ。
ちなみに、\$4003は、矩形波チャンネル1の音長カウンタを設定するレジスタだよ。他のチャンネルでも、対応するレジスタに同じ値をセットすれば、同じ長さの音が鳴るんだ。
矩形波チャンネル2:\$4007
三角波チャンネル:\$400B
ノイズチャンネル:\$400F
このように、音長カウンタの初期値を設定することで、音の長さを制御できるんだね。プログラムを書くときは、この計算式を使って、目的の音の長さになるように初期値を選ぶことが大切だよ。
なるほどね!
というわけで、対応表をちょっとだけつくる。
計算を単純化すると
(数値 + 1) / 15
音長カウンタの初期値(2進数) | 音長カウンタの初期値(10進数) | 音の長さ(秒) |
---|---|---|
%00000000 | 0 | 0.0000 |
%00000001 | 1 | 0.1333 |
%00000010 | 2 | 0.2000 |
%00000011 | 3 | 0.2667 |
%00000100 | 4 | 0.3333 |
%00000101 | 5 | 0.4000 |
%00000110 | 6 | 0.4667 |
%00000111 | 7 | 0.5333 |
%00001000 | 8 | 0.6000 |
%00001001 | 9 | 0.6667 |
%00001010 | 10 | 0.7333 |
%00001011 | 11 | 0.8000 |
%00001100 | 12 | 0.8667 |
%00001101 | 13 | 0.9333 |
%00001110 | 14 | 1.0000 |
%00001111 | 15 | 1.0667 |
%00010000 | 16 | 1.1333 |
%00010001 | 17 | 1.2000 |
%00010010 | 18 | 1.2667 |
%00010011 | 19 | 1.3333 |
%00010100 | 20 | 1.4000 |
%00010101 | 21 | 1.4667 |
%00010110 | 22 | 1.5333 |
%00010111 | 23 | 1.6000 |
%00011000 | 24 | 1.6667 |
%00011001 | 25 | 1.7333 |
%00011010 | 26 | 1.8000 |
%00011011 | 27 | 1.8667 |
%00011100 | 28 | 1.9333 |
%00011101 | 29 | 2.0000 |
%00011110 | 30 | 2.0667 |
%00011111 | 31 | 2.1333 |
音は最長でも2.13秒までしか出せないってことだね。
把握。
音量を徐々に小さくする設定
1 : 一定音量
0 : 徐々に音量減
フェードアウト設定。
音をなりっぱなしの設定にすると、音量最大→0→MAX→0を繰り返すループ再生になる。
音量
lda #%10001111 ;ここの下4桁
sta $4000
数値が大きいほど音量大
だけど、フェードアウト設定がオンになってる場合、音量が減る速度になる。
数値が大きいほど減少が遅くなる。
じゃー、フェードアウトの時の音量は調整できないってことやな?
$4001の内容
音の高さを上げ下げするやつ
上げ下げするやつってなんだよ?
一定の音量で鳴らすのでなく、ピロリロリロリ↑とかの、パワーアップ音的なやつみたいな。
グラディウスパワーアップしたわ的な。
設定項目は
%110011111なら
上の桁から
1 : 高さを変えるか?(1:ON 0:OFF)
100 : 一度に変わる量は?
1 :低くする?高くする?(1:高い 0:低い)
1111 :変える頻度は?
$4002の内容
音の周波数設定
まあ、音階の表現をここでするってこと。
lda #126
sta $4002
つまり
lda #%00000100 ; 周波数(下位8ビット)
sta $4002 ; 矩形波チャンネル1周波数レジスタ1
lda #%11111011 ; 再生時間・周波数(上位3ビット)
sta $4003 ; 矩形波チャンネル1周波数レジスタ2
こういうことらしい。
値が小さいほど高音、大きいほど低音になる。
ただし、周波数は\$4002の下位8ビットと、\$4003のビット2~0が上位3ビット。
上の例だと、%01100000100になる。
つまり\$4002の値だけを変えても音階は微妙に変わるだけで、 \$4003の下位3ビットが音階に大きな影響があるということになる。
ちなみに\$4003のビット7~3は再生時間で、\$4000の5ビット目を0にした場合に有効になる。
このように\$4015のサウンドレジスタと、矩形波チャンネルの4つのレジスタを設定すれば、矩形波音を鳴らすことができる。
ダッテサ。
音階との対応表はこちら
音階 | $4002 (下位8ビット) | $4002 (10進数) | $4003 (上位3ビット) |
---|---|---|---|
ド | %11100101 (0xE5) | 229 | %00000111 (7) |
ド# | %11011110 (0xDE) | 222 | %00000110 (6) |
レ | %11001100 (0xCC) | 204 | %00000110 (6) |
レ# | %10111100 (0xBC) | 188 | %00000110 (6) |
ミ | %10101110 (0xAE) | 174 | %00000101 (5) |
ファ | %10100010 (0xA2) | 162 | %00000101 (5) |
ファ# | %10000111 (0x87) | 135 | %00000101 (5) |
ソ | %01111100 (0x7C) | 124 | %00000100 (4) |
ソ# | %01110010 (0x72) | 114 | %00000100 (4) |
ラ | %01101000 (0x68) | 104 | %00000100 (4) |
ラ# | %01011111 (0x5F) | 95 | %00000011 (3) |
シ | %01010111 (0x57) | 87 | %00000011 (3) |
ド | %01001111 (0x4F) | 79 | %00000011 (3) |
※ この表は、ファミコンの主要な動作周波数である1.79MHzを基準にしている。
※ 音階は平均律で計算されています。
※ \$4003の上位3ビットは、\$4002の値と組み合わせて使用。
この表を使えば、ファミコンの矩形波チャンネル1で、各音階に対応する周波数を設定することができる。
例えば、"ド"の音を出すには、\$4002に0xE5を、\$4003の上位3ビットに7を設定。
周波数を倍にしたら1オクターブ下がるよ。
lda #126
sta $4002
lda #%00000000
sta $4003
これはラの音だけど、126*2にしたら、1オクターブ低いラが出るってことだね。
これで曲つくるの地獄か?
ちなみに、ほかのチャンネルと同時に鳴らすには
lda $4015 ; サウンドレジスタ
ora #%00000010 ; 矩形波チャンネル2を有効にする
sta $4015
こんなふうにorでつなげばいいらしい。
ふーん。
難しい
矩形波2について($4004~7)
矩形波1とちょっと違うらしい。
音を徐々に上げ下げする設定にしたときに、一定まで音が行った時に音が消えるか消えないかが違う。
2は消えないで1は消える。
1はピロリロリロリ!
に対して
2はピロリロリロリーーーーーーーーーーーー
っていう感じ。
三角波について
矩形波に比べて自由度が低い。
音量指定もできないし、周波数の変化指定もできない。
じゃあ何ができるんだ?って話
三角波を使用するには、\$4015の2ビット目を1にした後に、\$4008,\$400A,\$400Bを設定。
\$4008はリニアカウンターの設定をするレジスタ。音を鳴らす長さを設定します。
lda #%01111111 ; カウンタ使用・長さ
sta $4008 ; 三角波チャンネル制御レジスタ
ビット7が1だとリニアカウンター(なりっぱなし設定)を使用し、ビット6~0の数値が大きいほど長く鳴る。
ビット7を0にすると、後で設定する長さの分だけ再生される。
\$400Aと\$400Bで周波数を設定します。
lda #%00000100 ; 周波数(下位8ビット)
sta $400A ; 三角波チャンネル1周波数レジスタ1
lda #%11111011 ; 再生時間・周波数(上位3ビット)
sta $400B ; 三角波チャンネル1周波数レジスタ2
見ての通り、矩形波の周波数レジスタ$4002,$4003と全く同じです。三角波レジスタは以上3つ。単調な音しか出せない。
再生時間を表すところが2つあるじゃん!泣く!
ってところだけど、ここはどっちかが終わったら終了らしい。
短い方有線ってことで。
なんか、なんだろう、どういう時に使うかは使ってみないとわからないけど、重要なチャンネル数の一つだからな。
ノイズについて
最後にノイズ。
ドラム音や射撃音や爆発音などの効果音っぽいやつを出す。
ノイズを使用するには、\$4015の3ビット目を1にして、\$400C,\$400E,\$400Fを設定します。
\$400Cでは音の減衰を設定します。
$400C 音の減衰
lda #%11101111 ; 未使用2bit・鳴りっぱなし1bit・音量フェードアウト1bit・減衰率4bit
sta $400C ; ノイズ制御レジスタ
矩形波の\$4000に似てる、が、ビット7~6は未使用。
次の\$400E,\$400Fではタイプ、波長、長さを指定。
lda #%00000100 ; 乱数タイプ1bit・未使用3bit・更新頻度4bit(乱数をひく頻度。変えると、なんか、ノイズの種類がめっちゃかわる。パルス音的なものから爆発音的なものまで)
sta $400E ; ノイズ周波数レジスタ1
lda #%11111011 ; 再生時間5bit・未使用3bit
sta $400F ; ノイズ周波数レジスタ2
\$400Eの7ビット目は乱数タイプを選択。
0 : 32kbit
1 : 93kbit
\$400Eのビット3~0で周波数を選択。
\$400Fのビット7~3で音の長さを指定。
他のと同様。
ノイズの乱数タイプや周波数については・・・うん。
まあ、実際にやるときに調べよう。
差分PCM
\$4010~\$4013
どこかで録音してきた音を再生できる。らしい。
音声データはbank2($C000)に配置。
めちゃめちゃ低音質だけど再生できる模様。
音声ファイルはPCM形式。
MakeDPCMというツールがあって、wavファイルから変換できる。すげー。
これ
↓
http://www.ddss.sakura.ne.jp/
マニュアル:http://www.ddss.sakura.ne.jp/sw/makedpcm1-1-0-0/MakeDPCM_Manual.html
DPCMレートの数字は後で使うらしい。(動画内の説明)
レートは、低いほど間隔が荒くなるらしい。
じゃあ高い方がいいのかって話だけど、そこはわからん。(容量の関係か?)
まとめ
動画では最後にサンプリング音源を鳴らすためのなんかをしてるけど、まあそれはおいておこう。
一応コード
.inesprg 2
.ineschr 0
.inesmir 0
.inesmap 0
.bank 0
.org $8000
Start:
lda #%10000000
sta $2000
lda #0
sta $2001
mainProcess:
inc $00 ;フレームカウンター
lda $00
and #31 ;カウンター&31で論理積をとる
sta $00 ;論理積の結果をメモリ書き込み
cmp #31 ;31は2進数で%011111,32は%100000なので、ANDすると0に戻るというロジック
bne hage
lda #7
sta $4010
lda #31*2 ;初期電圧
sta $4011
lda #0 ;開始アドレス($C000 + x * 64)
sta $4012
infinity:
jmp infinity
.bank 1
.org $FFFA
.dw mainProcess
.dw Start
.dw 0
説明はわかったけど、コードに落とし込めてない・・・
ちょっと今回長かったけど、仕組みはなんとなーーーーく理解できた。
が、本当に初歩の初歩のしょほほほほくらい。
次からはより実践的なところに踏み込んでいく。
つまりは、画面を動かしたりのよりゲームに近い部分を試していく。
次の参考文献は、動画内で挙げている
NES研究所 https://hp.vector.co.jp/authors/VA042397/nes/index.html
ギコ猫でもわかるファミコンプログラミング http://gikofami.fc2web.com/index.html
を適当に見ていこうと思う。
ぬわー、大変だモォ
それではまた次回。