手のひらサイズのS98プレイヤーを作る
4周遅れくらいですが、僕も手のひらサイズのS98プレイヤーを作りました。
これは何?
SDカードに入れたS98ファイルをスタンドアロンで再生できるボードです。YMF288(OPL3)の実チップを使ってOPN3-L用の楽曲を再生できます。ボード上にマイコン(ATMEGA328)、Sdカードスロット(基板裏面)、YMF288、DAC、アンプ、操作ボタン、表示用LCDを搭載しています。
機能
- SDカード(FAT32対応)内のS98ファイルを再生可能
- OPN3-L用の楽曲データに対応します(PCMパートは非対応)
- S98v3対応(ただしPCMパート入りデータは非対応、今の所はFMパートのみの再生も不可)
- 操作ボタンを使って再生・停止・曲送り・頭出しが可能(現時点では未実装)
- LCDにファイル名・ステータスを表示可能
回路図・ソースコード
YMF288のピンアサイン、サンプルコードはVainDreams別館様を参考にさせていただきました。
ソースコードにビルドには以下のライブラリが必要です。
/*
YMF288 driver for YMF288 Player Unit
License: GNU General Public License V3
(C) Copyright 2015 TaisukeWatanabe(@fcneet)
【参考資料】
・YMF288-M ピン配列&回路 (http://d.hatena.ne.jp/m_yanase/20131007/1381151650)
・YMF288変換基板+Arduinoで簡易S98プレーヤを作る(その1~2)
(http://d.hatena.ne.jp/m_yanase/20130910/1378810438)
・S98v3フォーマット(http://www.vesta.dti.ne.jp/~tsato/soft_s98v3.html)
・S98プレイヤを作ってみる(http://risky-safety.org/zinnia/sdl/works/fmgen/s98sdl/)
*/
#include <SD.h>
#include <TimerOne.h>
#include "ring_buffer.h"
// リングバッファのサイズ
//
#define BUF_SIZE 256
static const int data_pins[] = { 0,1,2,3,4,5,6,7 }; // D0-D7
static const int RST = 8; // Reset
static const int A0_ = 17; // A0
static const int A1_ = 18; // A1
static const int WR = 15; // Read Enable
static const int CS = 14; // Chip Select
// SDカード CS Pin定義
const int chipSelect = 10;
// S98ファイルヘッダ構造体
//
struct S98Header {
char Magic[3];
char Format[1];
unsigned long Timer;
unsigned long Timer2;
unsigned long Compress;
unsigned long NamePtr;
unsigned long DataPtr;
unsigned long LoopPtr;
unsigned long DeviceCount;
unsigned long DeviceInfo;
};
// クラス OPN3L_MDL 宣言
//
class OPN3L_MDL{
public:
// コンストラクタ
OPN3L_MDL(){
}
// モジュールリセット
void reset(){
digitalWrite(RST, LOW); // モジュールリセット
delayMicroseconds(100); // min. 25us
digitalWrite(RST, HIGH);
delay(100);
}
// レジスタライト
void reg_write(unsigned char ifadr, unsigned char adr, unsigned char dat){
law_write(ifadr, adr, dat);
switch(adr){ // データライト後のWait
case 0x28:{ // FM Address 0x28
delayMicroseconds(26); // min: 24us wait
break;
}
case 0x10:{ // RHYTHM Address 0x10
delayMicroseconds(24); // min: 22us wait
break;
}
default:{ // Other Address
delayMicroseconds(3); // min.1.9us wait
}
}
}
private:
// データバスセット
void write_data(unsigned char dat){
PORTD = (PORTD&0B00000000) | dat; // ポートD0-D7へ出力データセット
}
// データライト
void law_write(unsigned char ifadr, unsigned char adr, unsigned char dat)
{
PORTC = (PORTC & ~0B01000); // A0 LOW(Address Bus set)
PORTC = (PORTC & ~0B10000) | (ifadr << 4); // A1 Set
write_data(adr); // Address set
PORTC = (PORTC & ~0B00001); // CS LOW
delayMicroseconds(1); // min: 200ns wait
PORTC = (PORTC | 0B00001); // CS HIGH
delayMicroseconds(3); // min: 1.9us wait
PORTC = (PORTC | 0B01000); // A0 HIGH(Address Bus set)
write_data(dat); // Data set
PORTC = (PORTC & ~0B00001); // CS LOW
delayMicroseconds(1); // min: 200ns wait
PORTC = (PORTC | 0B00001); // CS HIGH
}
};
// グローバル変数
//
OPN3L_MDL mdl; // GMC-MOD01制御クラス
File dir;
File dataFile; // SDカードアクセスクラス
S98Header header; // S98形式ヘッダ格納用構造体
rbuffer_t rbuf; // リングバッファ構造体
char buf[BUF_SIZE+1]; // リングバッファのバッファ領域
int loop_count = 0; // Sync waitカウント用変数
void attachTimerOne()
{
if(header.Timer==0)
Timer1.attachInterrupt(S98Play, 990 * 10);
else
Timer1.attachInterrupt(S98Play, 980 * header.Timer);
}
// Setup関数
//
void setup(){
int i;
for(i=0; i<8; i++) // Arduino Pin初期化
pinMode(data_pins[i], OUTPUT);
pinMode(A0_, OUTPUT);
pinMode(A1_, OUTPUT);
pinMode(WR, OUTPUT);
pinMode(CS, OUTPUT);
pinMode(RST, OUTPUT);
mdl.reset();
// SDカードアクセス初期化
pinMode(SS, OUTPUT);
if (!SD.begin(chipSelect)) {
while(1);
}
dir = SD.open("/");
dataFile = dir.openNextFile();
if (!dataFile)
{
while(1);
}
// S98ヘッダ情報取得
dataFile.seek(0x0);
dataFile.read(&header, sizeof(header));
// リングバッファ初期化&データ先頭へシーク
rbuffer_init( &rbuf, buf, BUF_SIZE+1 );
dataFile.seek(header.DataPtr);
digitalWrite(WR, LOW);
// タイマ割込宣言
// 1sync期間毎に割込を掛けるよう設定
Timer1.initialize();
attachTimerOne();
}
// Loop関数
//
void loop(){
}
// S98データ演奏関数
//
void S98Play(){
int n,s,i;
char devadr, dat, adrdat[2];
char buf_tmp[BUF_SIZE];
// リングバッファ内データサイズのチェック&データ積み込み
if(rbuffer_size(&rbuf)<(BUF_SIZE/4)){
dataFile.read(buf_tmp, (BUF_SIZE-rbuffer_size(&rbuf)));
rbuffer_write(&rbuf, buf_tmp, (BUF_SIZE-rbuffer_size(&rbuf)));
}
// Sync wait中はwaitカウントを減じて関数を抜ける
if(loop_count!=0){
loop_count--;
return;
}
do{
// リングバッファのデータが3byte未満の時は関数を抜ける
if(rbuffer_size(&rbuf)<3)
return;
rbuffer_read(&rbuf, &devadr, 1);
switch(devadr){
case 0x00:{
rbuffer_read(&rbuf, adrdat, 2);
mdl.reg_write( 0B0, adrdat[0], adrdat[1] );
break;
}
case 0x01:{
rbuffer_read(&rbuf, adrdat, 2);
mdl.reg_write( 0B0, adrdat[0], adrdat[1] );
break;
}
case 0xFF:{ // 1 sync wait
loop_count = 0;
return;
}
case 0xFE:{ // n sync wait
n = s = 0; i = 0;
do{
++i;
rbuffer_read(&rbuf, &dat, 1);
n |= (dat & 0x7f) << s;
s += 7;
} while(dat & 0x80);
n += 2;
loop_count = n-1;
return;
}
case 0xFD:{ // データ末尾
delayMicroseconds(2000 * 1000);
mdl.reset();
dataFile.close();
dataFile = dir.openNextFile();
if(!dataFile)
{
dir.rewindDirectory();
dataFile = dir.openNextFile();
}
// S98ヘッダ情報取得
dataFile.seek(0x0);
dataFile.read(&header, sizeof(header));
// リングバッファ初期化&データ先頭へシーク
rbuffer_init( &rbuf, buf, BUF_SIZE+1 );
dataFile.seek(header.DataPtr);
Timer1.detachInterrupt();
attachTimerOne();
return;
}
}
}while(1);
}
備考・今後の予定
- 現時点のソースコードではLCDと操作ボタンは動作しません
- YMF288は秋葉原のFM音源ガチャこと、FM音源伝説で手に入れました
- 現時点のソースコードはArduinoスケッチベタ書きのため、もう少し整理します
- もしかしたらキット化して頒布するかもしれません
前に作ったラズパイ用OPN(YM2151)ボードも何とかしなければ(´・ω・`)