MIS.W 公式ブログ

早稲田大学公認、情報系創作サークル「早稲田大学経営情報学会」(MIS.W)の公式ブログです!

ファミコンDAWの制作秘話 ~DAWがなければDAWを作ればいい~【カウントダウンカレンダー2018冬3日目】

ファミコンDAWの制作秘話
DAWがなければDAWを作ればいい~

キッカケ

 私はファミコンがとても大好きです(唐突なカミングアウト)。 なので、曲を作成する場合はファミコンのような3和音+1和音のみなどの制限のあるものにしようとしてしまいます。 ですが、その制限とその機械的特徴(音色)を再現するDAWもしくはVSTiが見つかりませんでした。 、なければ作ればいい だけの 話なので作っちゃいました!!

作るにあたって

 まず、作る前にファミコンの音源についての仕様を調べなければなりませんね、 仕様を知らなければ再現なんてできませんから。 で、調べた。

FC音源とは (ファミコンオンゲンとは) [単語記事] - ニコニコ大百科

なーんだただのニコニコ大百科じゃないか、と思ったそこのあなた!このサイト、一番詳しく書いてあったのです! なので仕様を確認。

音色 チャンネル数
矩形波
(duty比:12.5% 25% 50% 75%)
2
三角波
(16段階)
1
ノイズ
(長周期・短周期)
1
DPCM 1
ファミコン内蔵の音源。
初期RP2A03(A,Bボタンが四角のころ)では短周期ノイズが出せないうえ、一番低いノイズ音も一つ上の周波数と同じノイズ音が出てしまう。

と、 まぁ面倒くさそう 結構詳しい。これよりも詳しい説明はニコニコ大百科で。
 仕様を知ったので作り始めます。

試作品

 これを作り始めたのは高校生のころ。その頃はJavaにあまり詳しくなく、まずは音波を生成するだけでも一苦労。

public static byte[] Square(double Hz, float Duty, int sampleCount, double Exp)
{
    if(Hz == -1)
    {
        byte[] data = new byte[sampleCount];
        for(int i = 0; i < data.length; i++)
        {
            data[i] = 0;
        }
        return data;
    }
      byte[] data = new byte[sampleCount];
      for(int i = 0; i < data.length; i++)
      {
          double phase = i / (HzMu / Hz);
          phase -= Math.floor(phase);
          data[i] = (byte)(((phase <= Duty ? 127 : -128) / 127.0) * 100.0 * Exp);
      }
    return data;
}

これは矩形波の音波生成コードです。これ以外に、三角波、ノイズの波形を生成するコードも存在します。 これは割愛しますね。(クソ長コードみたいですか?)
 まずは再生機構、ということで、「プログラムに直書きされている曲を流すプログラム」を作成しました。
NESもどき これです。これはコマンドプロンプトで操作します。 これはあらかじめある長さの音波の波形を生成しておき、それを SourceDataLine に書き込み流しています。 SourceDataLine は書き込んだ波形が全て流れきるまで書き込みメソッドがLOCKされるので、流したい長さを流し終わった後に 次の音を鳴らす書き込み処理が入るようになっています。
 プログラムの公開はありません。

DAWに向けて

 DAWとは、Digital Audio Workstationの略です。 要するに、デジタルの環境(この場合はPC)を使用してAudio(曲など)を作る作業場、みたいな意味ですかね。 試作品では曲が制作できない(プログラムに直書きなため)のでDAWとは100歩譲っても言えません。 そこで、DAWとして機能させるために曲を打ち込むUIを作成します。これがなければ始まりませんね。
Thoone-fitst 作成しました。4Cとか書いてあるあそこが曲を打ち込むUIです。これにマウスでポチポチ音程と長さを入れていけば曲が完成します。 そこ、曲のセンスがないとか言わない

音を出す

 曲を打ち込めます、これだけでいいはずがありません。再生できなければ意味がありません! ということで、再生できるようにプログラムを変更していきます。 まずは、打ち込み時にその音程の音が鳴るようにし、次に再生時に打ち込まれたノーツ(あの緑の)に合わせて音が鳴るようにしていきます。
 ところで、あのノーツは以下のような配列の組み合わせで表現されています。

public ArrayList<Byte> Volume = new ArrayList<>();    //音量を保存する
public ArrayList<Double> Freque = new ArrayList<>();  //周波数(音程)を保存する
public ArrayList<Integer> FrequI = new ArrayList<>(); //0Cから数えて何番目かを保存する(DAWに緑のノーツを表示する位置を決定する)
public ArrayList<Long> Time    = new ArrayList<>();  //再生開始時間を保存する(サンプルを保存)
public ArrayList<Integer> SoundT = new ArrayList<>(); //鳴らし続ける時間を保存する(サンプルを保存)
public ArrayList<Float> Duty   = new ArrayList<>(); //デューティー比を保存する(矩形波)、短周期ノイズかを判断する(ノイズ)
public ArrayList<Double> Voldow = new ArrayList<>();  //音量のスイープ率を保存する(矩形波、ノイズ)
public ArrayList<Double> Fredow = new ArrayList<>();  //音程のスイープ率を保存する
public ArrayList<Integer> VolDUM = new ArrayList<>(); //音量のスイープ限界を保存する(途中で止める場合に使用)

これらを使用して、4チャンネルで同期しながら (本当はしていないけど) 再生します。 機能的にはSpaceボタンで再生、停止です。

保存する

 再生できるだけではだめです。 保存できないとか発狂ものです。せっかく作ったのに消えてしまう、それではいけません。 セーブロード機能をつけましょう。で、できたものがこちら。保存形式です。

126.0
140.0
126.0
140.0
0,16,220.0,45,0,11428,0.5,-2.0E-4,1.0
0,16,659.2551138257398,64,11428,11428,0.5,-2.0E-4,1.0
0,16,587.3295358348151,62,22857,11428,0.5,-2.0E-4,1.0
0,16,659.2551138257398,64,34285,11428,0.5,-2.0E-4,1.0
0,16,523.2511306011972,60,45714,11428,0.5,-2.0E-4,1.0
0,16,659.2551138257398,64,57142,11428,0.5,-2.0E-4,1.0
0,16,493.8833012561241,59,68571,11428,0.5,-2.0E-4,1.0
0,16,659.2551138257398,64,80000,11428,0.5,-2.0E-4,1.0
0,16,440.0,57,91428,91428,0.5,-2.0E-4,1.0
0,16,146.8323839587038,38,182857,11428,0.5,-2.0E-4,1.0
0,16,698.4564628660078,65,194285,11428,0.5,-2.0E-4,1.0
0,16,659.2551138257398,64,205714,11428,0.5,-2.0E-4,1.0
0,16,698.4564628660078,65,217142,11428,0.5,-2.0E-4,1.0
0,16,587.3295358348151,62,228571,11428,0.5,-2.0E-4,1.0
0,16,698.4564628660078,65,239999,11428,0.5,-2.0E-4,1.0
0,16,523.2511306011972,60,251428,11428,0.5,-2.0E-4,1.0
0,16,698.4564628660078,65,262857,11428,0.5,-2.0E-4,1.0
0,16,493.8833012561241,59,274285,91428,0.5,-2.0E-4,1.0
0,16,220.0,45,365714,11428,0.5,-2.0E-4,1.0
...

なぁにこれぇ?汚いですね。本当はmidi規格にすればいいのでしょうが、主が

midi?何それおいしいの?」

とかいうから独自規格になりました。あ、.wavでも出力できるので安心してくださいね。

 保存については、

private void Save(Thoone th)
{
  FileWriter fw2 = null;
  try
  {
    fw2 = new FileWriter("./MUSIC.thh");
  }
  catch (IOException e)
  {
    try
    {
      Files.createFile(Paths.get("./MUSIC.thh"));
      fw2 = new FileWriter("./MUSIC.thh");
    }
    catch (IOException e1)
    {
      e1.printStackTrace();
    }
  }
  this.write(fw2, th);
  this.time[0] = System.currentTimeMillis();
}

private void write(FileWriter file, Thoone th)
{
  try
  {
    String str = "";
    for(int i = 0; i < 4; i++)
    {
      if(i == 2) str += th.ch.loopstartnum + "\r\n";
      else str += th.SPT[i].Tempo + "\r\n";
    }
    for(int i = 0; i < 4; i++)
    {
      for(int j = 0; j < th.SPT[i].Volume.size(); j++)
      {
        str += i + "," + th.SPT[i].Volume.get(j) + "," + th.SPT[i].Freque.get(j) +
               "," + th.SPT[i].FrequI.get(j) + "," + th.SPT[i].Time.get(j) + "," +
               th.SPT[i].SoundT.get(j) + "," + th.SPT[i].Duty.get(j) + "," +
               th.SPT[i].Voldow.get(j) + "," + th.SPT[i].Fredow.get(j) + "," +
               th.SPT[i].VolDUM.get(j) + "\r\n";
      }
    }
    file.write(str);
    file.close();
  }
  catch (IOException e)
  {
    e.printStackTrace();
  }
}

と、ただ単純に音を出すの項目で述べた"配列の組み合わせ"をカンマ区切りで保存しているだけです。

完成!

 とまぁ、ここまで出来れば完成といっても過言ではないでしょう。 ただ、実際はもっといらない機能をたくさんつけました。 波形表示窓とか。

 で、完成品がこれです。ビデオで見たらいい感じにわかるかなって。

 再生位置を分からせるためのバーには苦労しました。

作り終えて

 これを作成しようとした動機は一番上にあるのですが、これが完成するまで結構長い年月が経ってしまいました。(2年くらいかな) ただ、完成してよかったです。なければ作ればいいという発想は捨てるべきではないと思うので、創作意欲のあるかたもないかたも何か作ってみては?
あ、ちなみに完成品はこちら。特に公開に制限等をかけないので勝手に使ってもらって大丈夫ですよ。

翌日は

 明日はくまちゃんさんの「デスマ回避!分析設計のススメ!」ですね。私は設計できないので参考にしようかなと。 デスマ研を引き継ぐものとしてはデスマ回避!はいただけなかったり?