NSFプレイヤーをつくりたい
NSF = NES Sound Format
ファミコンから音のデータを取り出したもの
資料集
NSFの仕様
NesDev
ファミコンエミュレータ
わかりやすそうなファミコンエミュレータ作成のチュートリアル
Introduction - Writing NES Emulator in Rust
アドレッシングモード
CPU 6502のアセンブリ
Playground
逆アセンブルもできる
レファレンス
この音楽データがどういう形式なのかわからなかったが、どうやら音に関わる機械語のコードをゲームソフトから取り出したものらしい。
$080 nnn ---- The music program/data follows
ということはファミコンのCPUをエミュレートする必要がありそう。
変更履歴:ファミコンのプログラミング入門記事で使えそうなものを複数資料集に追加。
使用言語は勉強中のOCamlを使うことにした。
ファミコンのCPUおよびAPUの仕組みをおおよそ理解したので、実装していく。
とはいえ実際にファミコンやOCamlの知識に抜け漏れがあり、想定外の地雷を踏まないために既存のNESエミュレータの実装を参考にしながら、書いていくことに。
一旦、音楽の出力に使うSDLが使えるか確認したい。
プロジェクト作成
dune init proj ocaml_nsf_player
bin/dune
(executable
(public_name ocaml_nsf_player)
(name main)
(libraries tsdl))
open Tsdl
let () = match Sdl.init Sdl.Init.(timer + audio) with
| Error _ -> print_endline "Hello"
| Ok () -> print_endline "World"
SDLが初期化できればWorld
と出力するプログラム
dune build
dune exec ocaml_nsf_player
うまくいった。
実際にSDLで音を鳴らす試験
音を鳴らす機能をaudio_backend.ml
に実装する
open Tsdl
type t =
{
device : int32;
sampling_freq : int;
}
type t_audio = float array
let create () : t =
match Sdl.init Sdl.Init.audio with
| Error _ ->
print_string "Failed while SDL initialization.";
assert false
| Ok () ->
let audio_spec =
{
Sdl.as_freq = 44100;
as_format = Sdl.Audio.f32;
as_channels = 1;
as_silence = 0;
as_samples = 1024;
as_size = Int32.zero;
as_callback = None;
}
in
let device, obtained_spec =
match Sdl.open_audio_device None false audio_spec Sdl.Audio.allow_frequency_change with
| Error _ ->
print_string "Failed while SDL initialization.";
assert false
| Ok ds -> ds
in
Sdl.pause_audio_device device false;
{
device = device;
sampling_freq = obtained_spec.as_freq;
}
let play_sound (backend : t) (samples : t_audio) : unit =
let
buffer = Bigarray.Array1.create Bigarray.float32 Bigarray.c_layout (Array.length samples)
in
Array.iteri (fun i sample -> buffer.{i} <- sample) samples;
match Sdl.queue_audio backend.device buffer with
| Ok () ->
while Sdl.get_queued_audio_size backend.device > 0 do
Sdl.delay 10l
done
| Error _ ->
print_string "Error while playing audio.";
assert false
let close (backend : t) : unit =
Sdl.close_audio_device backend.device;
Sdl.quit ()
これを使って三角波を鳴らすmain.ml
let generate_sine_wave (freq : float) (sampling_freq : int) (length : float) : float array =
let sample_count = int_of_float (float_of_int sampling_freq *. length) in
let samples = Array.make sample_count 0.0 in
let angle = 2.0 *. Float.pi *. freq /. float_of_int sampling_freq in
for i = 0 to sample_count - 1 do
samples.(i) <- sin (angle *. float_of_int i)
done;
samples
let () =
let backend = Audio_backend.create () in
let samples = generate_sine_wave 440.0 backend.sampling_freq 1.0 in
Audio_backend.play_sound backend samples;
Audio_backend.close backend
参考にしているnes-ml
レポジトリでは音楽の演奏をMake
のoutput_frame
で行っている。
module Make (A : Backend) : S = struct
...
let output_frame t =
let buffer = Resampler.resample t.resampler in
A.queue_audio t.backend buffer
...
end
これはnes.ml
のrun
で、各コンポーネントのサイクルを進めたあと描画結果が成功したら呼ばれているようにみえる。
let run t =
...
let rec aux frame =
if G.continue t.io.main_window then
if G.shown t.io.main_window then (
...
A.next_cycle t.state.apu;
...
(match P.should_render t.state.ppu with
| None -> ()
| Some bg_color ->
A.output_frame t.state.apu;
I.next_frame t.state.input;
矩形波を小さな部品から組み立てていく
矩形波のためにDuty比を実装する
ニコニコ大百科にあるようにファミコンのDuty比は、単に位相の上と下の比だけでなく波形がしっかり決まっている
例:Duty比 12.5%
_| ̄|_______| ̄|_______
つまり8回で一周し、その中身は「下 上 下 下 下 下 下 下」
これをnext
を呼ばれると内部のカウンタの値を上げていき、周期的にリセットするデータ構造のシーケンサの具体的な実装として定義できないかと考えたが、なかなかうまくいかない
最終的には、以下のようにファンクターを使って実装
Sequencerの定義:実際のget
の実装をモジュール`Getter`を通じて与える
module type Sequencer = sig
type t
val create : int -> t
val get : t -> int
val next : t -> unit
end
module Make_Sequencer (Getter : sig
val get_impl : int -> int
end) : Sequencer = struct
type t =
{
length : int;
mutable step : int;
}
let create length =
{
length;
step = 0;
}
let next t =
if t.step = t.length - 2
then t.step <- 0
else t.step <- t.step + 1
let get t = Getter.get_impl t.step
end
実際の`get`の実装
本当はここで具体的なDuty比からくる位相を出せると思っていた
module SimpleGetter = struct
let get_impl step = step
end
module SimpleSequencer = Make_Sequencer(SimpleGetter)
Dutyの実装
Sequencerを内部にもち、Duty比の種類におうじてget
の値を変える
type t =
{
mutable duty_type : int;
sequencer : SimpleSequencer.t;
}
let duties =
[|
[| 0; 1; 0; 0; 0; 0; 0; 0 |];
[| 0; 1; 1; 0; 0; 0; 0; 0 |];
[| 0; 1; 1; 1; 1; 0; 0; 0 |];
[| 1; 0; 0; 1; 1; 1; 1; 1 |];
|]
let create () =
{
duty_type = 0;
sequencer = SimpleSequencer.create 8
}
let update_duty_type t duty_type =
t.duty_type <- duty_type
let get t =
duties.(t.duty_type).(SimpleSequencer.get t.sequencer)
let next t =
SimpleSequencer.next t.sequencer
もとはDuty0
, Duty1
, Duty2
, Duty3
というモジュールを作る実装を考えていた
module Duty0Getter = struct
let table = [| 0; 1; 0; 0; 0; 0; 0; 0 |]
let get_impl step = table.(step)
end
module Duty0 = Make_Sequencer(Duty0Getter)
しかしこれではDuty0
, Duty1
, Duty2
, Duty3
それぞれが別のモジュールとなり、Duty比の切り替えるための型を作るのが難しくなってしまう
あえてそこまで複雑にすることもないか、とSequencerを内部に隠蔽することで妥協
しかし、多くの関数がSequencer
をそのまま読んでいるだけなのに別の実装になってしまうのがやや汚い
それらしい矩形波生成のモジュールはできたものの音がぷつぷつきれてしまう
おそらく位相をまとめてではなくて一つ一つ書いていることが原因と思われ、ある程度位相のデータがたまったらまとめて出力するようにしたい。
どれだけ溜めていいのか考えるために音を出力するまでの間に何回音を貯める処理をしているか、参考にしているエミュレータの実装を分析する。
音を貯める処理:
-
Resampler.buffer_next
で追加する。- 一回呼ばれるごとにキューブ補間が完了するまでサンプルをためる
- これはAPUの
next_cycle
で一度呼ばれる
-
next_cycle
は1フレームごとによばれる- このタイミングで音を変える処理なども行う
- Ricoh製の場合、1秒間に60フレームはしる
音を取り出す処理:
-
output_fram
で取り出して - これは1フレームごとに1度呼ばれる
つまり一度にキューブ補間が完了するまで位相をためて60秒に1度出力?
いろいろ詰まったが、ふと思いついてDeepSeekに矩形波のモジュールを作らせてみたら完璧に動いてしまった。
科学の力ってすげー
type t = {
mutable rate : int;
mutable key_off_counter : int;
mutable sweep_enabled : bool;
mutable sweep_shift : int;
mutable sweep_negate : bool;
mutable sweep_period : int;
mutable duty_cycle : int;
mutable frequency : int;
mutable length_counter : int;
mutable timer : int;
mutable phase : int;
mutable output : float;
}
let create () = {
rate = 0;
key_off_counter = 0;
sweep_enabled = false;
sweep_shift = 0;
sweep_negate = false;
sweep_period = 0;
duty_cycle = 0;
frequency = 0;
length_counter = 0;
timer = 0;
phase = 0;
output = 0.0;
}
let update_registers t addr value =
match addr with
| 0x4000 ->
t.duty_cycle <- (value lsr 6) land 0x03;
t.length_counter <- value land 0x3F;
| 0x4001 ->
t.sweep_enabled <- (value land 0x80) <> 0;
t.sweep_period <- (value lsr 4) land 0x07;
t.sweep_negate <- (value land 0x08) <> 0;
t.sweep_shift <- value land 0x07;
| 0x4002 ->
t.frequency <- (t.frequency land 0xFF00) lor value;
| 0x4003 ->
t.frequency <- (t.frequency land 0x00FF) lor ((value land 0x07) lsl 8);
t.key_off_counter <- (value lsr 3) land 0x1F;
t.phase <- 0;
| _ -> ()
let sample_one t =
if t.timer = 0 then begin
t.timer <- (2048 - t.frequency) * 2;
t.phase <- (t.phase + 1) land 0x07;
let duty_pattern = match t.duty_cycle with
| 0 -> 0b00000001
| 1 -> 0b10000001
| 2 -> 0b10000111
| 3 -> 0b01111110
| _ -> 0
in
t.output <- if (duty_pattern lsr t.phase) land 1 <> 0 then 1.0 else -1.0;
end else begin
t.timer <- t.timer - 1;
end;
t.output
一旦、ほかの音も同じ要領で作ってみて、うまくいっていそうだったら統合していく方針にする。
使用している可変長の配列を実現するライブラリ
module Appendable : sig
type 'a t
val create : int -> 'a t
val append : 'a t -> 'a -> unit
val flush : 'a t -> 'a array
end = struct
type 'a t =
{
data : 'a array;
mutable size : int;
capacity : int;
}
let create capacity =
if capacity <= 0 then
invalid_arg "Capacity must be a positive integer."
else
{
data = Array.make capacity (Obj.magic 9);
size = 0;
capacity = capacity;
}
let append t value =
if t.size >= t.capacity then
invalid_arg "The size exceeds the Appendable capacity.";
t.data.(t.size) <- value;
t.size <- t.size + 1
let flush t =
let result = Array.sub t.data 0 t.size in
t.size <- 0;
result
end
APU
open My_lib.Appendable
let volume = 15.0
type t =
{
square1 : Square.t;
backend : Audio_backend.t;
clock : Clock.t;
samples : float Appendable.t
}
let create () : t =
let backend = Audio_backend.create () in
{
square1 = Square.create ();
backend = backend;
clock = Clock.create 1;
samples = Appendable.create 60000;
}
let mix t =
let square_wave1 = Square.sample_one t.square1 in
let square_out = 95.88 /. ((8128. /. square_wave1) +. 100.) in
volume *. square_out
let write t addr value =
Square.update_registers t.square1 addr value
let next_cycle t =
Appendable.append t.samples (mix t)
let play_next_cycle t =
Audio_backend.play_sound t.backend (Appendable.flush t.samples)
これで音が鳴る
let () =
let apu = Apu.create () in
let rec loop cnt =
if cnt <= 0 then (Apu.play_next_cycle apu; exit 0) else
Apu.next_cycle apu;
loop (cnt - 1);
in
Apu.write apu 0x4000 0x80;
Apu.write apu 0x4001 0x88;
Apu.write apu 0x4002 0xF0;
Apu.write apu 0x4003 0x07;
loop 44100;