80円PSGをWindowsから鳴らしてみる

  • 5
    いいね
  • 0
    コメント

前提となるお話

80円のARMチップをPSG音源やSCC音源としてI2C経由で制御できるようにしてみた、という記事を以前投稿しました。

この手の作業をしている時はだいたいLinuxなんですが、最近リビングでWindowsのラップトップを使っている事が多いのでWindowsからI2Cを使えるようにi2c-tiny-usbを作ってみました、というのが前回のお話。

で、今回は実際にi2c-tiny-usb経由で音源チップで演奏する環境を作る話。libkssとlibusbを使ってcygwin環境でKSSファイルを再生できるようにしてみます。ここまで揃うと音源チップのデバッグも楽になりますね。

開発準備

cygwin環境

cygwin自体についての説明は今回は省かせていただきます。gccやmake、CMake系のパッケージは一通り必要ですので揃えておいてください。今時はapt-cyg使うと楽らしい。これでもうsetup.exe捨てられる。apt-getと微妙にコマンド体系違うのが気になるけど、実際めちゃくちゃ便利。

libusb

libusbに関してはlibusb-develというパッケージを入れてあれば開発できるはず。libusb1.0-develで入るのは/usr/include/libusb-1.0/libusb.hで、i2c-tiny-usbが使っているのは/usr/include/usb.hの0.1系(だけど、わりと古いAPIを使い続けている/いたプロジェクトが多い)。USB周りでコンパイル通らなかったらこの辺りを疑ってみてください。
i2c-tiny-usbのデバイスに対してもlibusb-win32のフィルタドライバがインストールされている必要があります(i2c-tiny-usbをダウンロードするとwinディレクトリにドライバが入っています)。

libkss

digital-sound-antiquesさんがGitHubで管理しているものをforkしてI2C対応してみました。ライブラリのbuildだけならほぼ手直しなしで以下の手順でできます。ただ、これだけでは何もできないので、ライブラリを使ったプレイヤを制作していく必要があります。

% git clone https://github.com/toyoshim/libkss.git
% cd libkss
% git submodule update --init
# CMakeLists.txtのcmake_minimum_required(VERSION 3.4)を~(VERSION 3.3.4)に修正
% cmake .
% make

変更の要

libkssを使った簡易プレイヤーの作成

cygwinなら昔ながらの/dev/dspがカジュアルに使えるので、ほんの50行くらいで演奏するコードがかけます。sound_cortexブランチにkssplay_dsp.cとして入れてありますが、ざっくり以下の様な感じ。

kssplay_dsp.c
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/soundcard.h>
#include <unistd.h>

#include "kss/kss.h"
#include "kssplay.h"

int main(int argc, char** argv) {
  if (argc < 2) {
    fprintf(stderr, "Usage: %s <filename> [<song id>]\n", argv[0]);
    return 1;
  }

  // /dev/dspの初期化
  int dsp = open("/dev/dsp", O_RDWR);
  const int channels = 2;
  const int frequency = 44100;
  const int format = AFMT_S16_LE;
  if (dsp < 0 ||
      ioctl(dsp, SNDCTL_DSP_CHANNELS, &channels) < 0 ||
      ioctl(dsp, SNDCTL_DSP_SPEED, &frequency) < 0 ||
      ioctl(dsp, SNDCTL_DSP_SETFMT, &format) < 0) {
    perror("/dev/dsp initialization");
    return 1;
  }

  // KSSPLAYも同じ条件で初期化
  KSSPLAY* kssplay = KSSPLAY_new(44100, 2, 16);

  // KSSファイル読み込み
  KSS* kss = KSS_load_file(argv[1]);
  if (NULL == kss) {
    fprintf(stderr, "%s: KSS_load_file failed\n", argv[1]);
    return 1;
  }

  // データセット後にresetで曲番号を指定
  KSSPLAY_set_data(kssplay, kss);
  KSSPLAY_reset(kssplay, 2 < argc ? atoi(argv[2]) : 0, 0);

  // VSYNC相当単位で波形生成、書き出しのループを回す
  uint16_t buffer[735 * 2];
  for (;;) {
    KSSPLAY_calc(kssplay, buffer, 735);
    // 再生が詰まるとブロックするのでタイミングは勝手に調整される
    write(dsp, buffer, 735 * 4);
  }
  return 0;
}

libkssはエミュレーションするデバイス毎にライブラリ化されているので、ざっくり以下のような感じでリンクしてあげます。

Makefile.dsp
LIBS= -L. -lkss \
     -Lmodules/emu2149 -lemu2149 \
     -Lmodules/emu2212 -lemu2212 \
     -Lmodules/emu2413 -lemu2413 \
     -Lmodules/emu8950 -lemu8950 \
     -Lmodules/emu76489 -lemu76489 \
     -Lmodules/kmz80 -lkmz80
INCS=-Imodules/emu2149 \
     -Imodules/emu2212 \
     -Imodules/emu2413 \
     -Imodules/emu8950 \
     -Imodules/emu76489 \
     -Imodules/kmz80 \
     -Isrc

kssplay_dsp.exe: kssplay_dsp.c
        gcc $< -o $@ $(LIBS) $(INCS)

libusbを使ったemu2149の置き換え

で、PSG部分を担当するemu2149のコードを置き換えてやれば、同プレイヤーからSoundCortex経由で演奏できるはず。先のコードよりは少し長くなるけど、それでも100行はいかないくらいで書けます。

sound_cortex.c
#include <stdio.h>
#include <string.h>
#include <usb.h>

#include "emu2149.h"

// この手の関数は無視して大丈夫
void PSG_set_quality(PSG* psg, uint32_t quality) {}
int16_t PSG_calc(PSG* psg) { return 0; }
void PSG_setVolumeMode(PSG* psg, int mode) {}
uint32_t PSG_setMask(PSG* psg, uint32_t mask) { return 0; }
void PSG_reset(PSG* psg) {}
void PSG_delete(PSG* psg) {}

// デバイスは1つだし、複数インスタンスをサポートする必要もないでしょう
static PSG psg;
static usb_dev_handle* i2c = NULL;

PSG *PSG_new (uint32_t c, uint32_t r) {
  memset(&psg, 0, sizeof(psg));

  // 2度目以降は何もしない
  if (i2c)
    return &psg;

  // VIDとPIDを頼りにi2c-tiny-usbを探す
  usb_init();
  usb_find_busses();
  usb_find_devices();
  for (struct usb_bus* bus = usb_get_busses(); bus; bus = bus->next) {
    for (struct usb_device* dev = bus->devices; dev; dev = dev->next) {
      if (dev->descriptor.idVendor == 0x0403 &&
          dev->descriptor.idProduct == 0xc631) {
        i2c = usb_open(dev);
        break;
      }
    }
  }
  if (!i2c) {
    fprintf(stderr, "i2c-tiny-usb: not found\n");
    return &psg;
  }
  // delayは10くらいを設定しておくと100kHz付近でI2Cが動作するが、ここでは待ちなし最速設定
  const int cmd_delay = 2;
  const int delay = 0;
  if (usb_control_msg(
        i2c, USB_TYPE_VENDOR, cmd_delay, delay, 0, NULL, 0, 1000) < 0) {
    fprintf(stderr, "i2c-tiny-usb: delay configuration failed: %s\n",
        usb_strerror());
  }
  return &psg;
}

void PSG_writeIO(PSG* psg, uint32_t adr, uint32_t val) {
  // adr 0でアドレス設定、adr 1で値書き込みという音源チップ定番インタフェース
  if (adr & 1) {
    if (psg->adr > 15)
      return;
    const int out = USB_TYPE_CLASS;
    const int cmd_wr = 7;
    char msg[2];
    msg[0] = psg->adr;
    msg[1] = val;
    // cmd_wrはi2c-tiny-usbが認識するI2C書き込みコマンド
    // 0x50はSoundCortexのI2Cのアドレス
    // msgに格納する2バイトはSoundCortexが認識する内部レジスタ更新手順の[addr, data]
    if (usb_control_msg(i2c, out, cmd_wr, 0, 0x50, msg, 2, 1000) < 1) {
      fprintf(stderr, "i2c-tiny-usb: %s\n", usb_strerror());
      return;
    }
    const int in = USB_TYPE_CLASS | USB_ENDPOINT_IN;
    const int cmd_rc = 3;
    char rc;
    // cmd_rcはi2c-tiny-usbが認識する直前のオペレーション終了ステータス確認コマンド
    // 成功時には1が帰る
    if (usb_control_msg(i2c, in, cmd_rc, 0, 0, &rc, 1, 1000) < 0 || rc != 1) {
      fprintf(stderr, "i2c-tiny-usb: %s\n", usb_strerror());
      return;
    }
  } else {
    // PSG側のレジスタは構造体内に格納しておく
    psg->adr = val & 0x1f;
  }
}

uint8_t PSG_readIO(PSG* psg) { return psg->reg[psg->adr]; }

タイミング制御

/dev/dspによる再生が不要になる分、書き込みのブロックでタイミングを調整する事ができなくなります。ここではcygwin環境でも使えるusleepを使った簡単なタイミング調整を紹介します。

kssplay_i2c.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <unistd.h>

#include "kss/kss.h"
#include "kssplay.h"

int main(int argc, char** argv) {
  if (argc < 2) {
    fprintf(stderr, "Usage: %s <filename> [<song id>]\n", argv[0]);
    return 1;
  }

  KSSPLAY* kssplay = KSSPLAY_new(44100, 2, 16);

  KSS* kss = KSS_load_file(argv[1]);
   if (NULL == kss) {
    fprintf(stderr, "%s: KSS_load_file failed\n", argv[1]);
    return 1;
  }

  KSSPLAY_set_data(kssplay, kss);
  KSSPLAY_reset(kssplay, 2 < argc ? atoi(argv[2]) : 0, 0);

  struct timeval now, next, tick, diff;
  gettimeofday(&now, NULL);
  tick.tv_sec = 0;
  tick.tv_usec = 1000 * 1000 / 60;

  for (;;) {
    // このフレームの処理を完了する期待時刻を計算
    timeradd(&now, &tick, &next);
    // 1フレーム分のエミュレーション実施(SoundCortexのレジスタ更新もここで発生する)
    KSSPLAY_calc_silent(kssplay, 735);

    // 期待される時刻より早い場合は差分だけsleepする
    gettimeofday(&now, NULL);
    if (timercmp(&now, &next, <)) {
      timersub(&next, &now, &diff);
      usleep(diff.tv_usec);
    }
    // 次の期待経過時間を計算する起点として更新
    now = next;
  }
  return 0;
}

Makefileはリンクするライブラリを一部差し替える程度なので、/dev/dsp版とまとめてこんな感じでどうでしょう。

Makefile.kssplay
LIBS= -L. -lkss \
     -Lmodules/emu2212 -lemu2212 \
     -Lmodules/emu2413 -lemu2413 \
     -Lmodules/emu8950 -lemu8950 \
     -Lmodules/emu76489 -lemu76489 \
     -Lmodules/kmz80 -lkmz80
INCS=-Imodules/emu2149 \
     -Imodules/emu2212 \
     -Imodules/emu2413 \
     -Imodules/emu8950 \
     -Imodules/emu76489 \
     -Imodules/kmz80 \
     -Isrc

kssplay_i2c.exe: kssplay_i2c.c sound_cortex.c
        gcc $^ -o $@ $(INCS) $(LIBS) -lusb

kssplay_dsp.exe: kssplay_dsp.c
        gcc $< -o $@ $(INCS) $(LIBS) -Lmodules/emu2149 -lemu2149

all: kssplay_i2c.exe kssplay_dsp.exe

まとめ

GitHubにブランチを作って以上の修正をcommitしてあります。libkssは結構ポータブルに作られているので、他の環境でもちょっとした手直しで遊べるかと思います。自分は昔PS2上でデバッグに使ったりしてました。

IMG_20160811_210640.jpg

おまけ

Visual Studio 2015からlibusb-1.0を使ってbuildするケースも対応してみました。MSXplugからそのままリンクしてWinAmpから使えるようにもなっているのですが、WinAmpがストリーム再生前提のため、タイミングをとるすべがなさそうです。
libusb-1.0を使う場合には、ドライバはWinUSBを使う必要があるようです。