Zenn
Open16

NSFプレイヤーをつくりたい

やおふぁいやおふぁい

NSF = NES Sound Format
ファミコンから音のデータを取り出したもの

資料集

NSFの仕様

NesDev
https://www.nesdev.org/wiki/NSF

ファミコンエミュレータ

わかりやすそうなファミコンエミュレータ作成のチュートリアル
Introduction - Writing NES Emulator in Rust
https://bugzmanov.github.io/nes_ebook/index.html

アドレッシングモード

https://zenn.dev/szktty/articles/nes-addressingmode

CPU 6502のアセンブリ

Playground

逆アセンブルもできる
https://skilldrick.github.io/easy6502/

レファレンス

https://www.nesdev.org/obelisk-6502-guide/reference.html

http://www.6502.org/tutorials/6502opcodes.html

やおふぁいやおふぁい

https://www.nesdev.org/wiki/NSF

この音楽データがどういう形式なのかわからなかったが、どうやら音に関わる機械語のコードをゲームソフトから取り出したものらしい。

$080    nnn ----    The music program/data follows

ということはファミコンのCPUをエミュレートする必要がありそう。

やおふぁいやおふぁい

変更履歴:ファミコンのプログラミング入門記事で使えそうなものを複数資料集に追加。

やおふぁいやおふぁい

使用言語は勉強中のOCamlを使うことにした。
ファミコンのCPUおよびAPUの仕組みをおおよそ理解したので、実装していく。
とはいえ実際にファミコンやOCamlの知識に抜け漏れがあり、想定外の地雷を踏まないために既存のNESエミュレータの実装を参考にしながら、書いていくことに。
https://github.com/Firobe/nes-ml/tree/main

やおふぁいやおふぁい

一旦、音楽の出力に使う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レポジトリでは音楽の演奏をMakeoutput_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.mlrunで、各コンポーネントのサイクルを進めたあと描画結果が成功したら呼ばれているようにみえる。

  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回で一周し、その中身は「下 上 下 下 下 下 下 下」

https://dic.nicovideo.jp/a/fc音源

これを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;
ログインするとコメントできます