はじめに
私は Fairy Computer System 80 (FCS80) という新しいレトロゲーム機のエミュレータを開発中ですが、そのゲーム機に SCC (SOUND CREATIVCE CHIP) を搭載してみたので、SCCの使い方の紹介がてらFCS80の ステルスマーケティング 紹介 をしてみます。
FCS80
ライセンスは利用料等一切不要で商用利用可能なクリアな形(MITライセンス)にしているので、FCS80用に開発したゲームは Steam なり Nintendo Switch なりでご自由に販売して頂けます。
FCS80 は、CPUにZ80(Z80A相当)を搭載して、独自開発のVDP(FCS80-VIDEO)、AY-3-8910 (PSG) などのデバイスを搭載しています。実機ハードウェアの開発はしてませんが、80年代後半〜90年代前半頃のテクノロジー水準で実機開発可能(な筈)な状態にしてあります。(スプライトの水平上限が256枚という部分に限ればやや怪しいかもしれませんが)
VDP(Video Display Processor ※現代でいうところのGPU)を除けば80年代前半にある既製品の寄せ集めです。
VDPはセガマークIIIとメガドライブの中間的なテクノロジー水準を目指して開発したので、音源がPSGのみだと若干バランスが悪いということで、音源チップのエンハンスの方向性を検討した結果、SCC完全互換の波形メモリ音源を搭載することにしました。
レトロゲーム機の王道といえば FM音源 かもしれませんが...
個人的にFM音源はゲーム機用としては少しオーバースペックかなと思っている節があります。(これについては異論が多いかもしれないので飽くまでも私見です)
エミュレータの実装もそこそこ大変です。
ただし、FM音源の中でも比較的単純なOPLL(2オペレータ)のエミュレータが下記に MITライセンス で公開されているので、自前主義に拘らなければエミュレータ開発自体はそれほど大変ではありません。
ですが、音源を扱うプログラム(音源ドライバ)や音源に食わせるデータ生成ツール(MMLコンパイラ)などのトータルの開発で見るとややオーバースペックかなということで、もっと単純でそれでいて十分な表現能力の高さのあるチップチューン音源を模索した結果、SCCを採用してみました。
KONAMIから怒られるかも?と思ったので独自調査をしてみたのですが、知的財産権上、KONAMIが著作権を有するプログラム(IPLやBIOS)を使用しておらず、商標登録も無さそうで、特許は未調査ですが特許登録されていたとしても既に期限切れの筈なのでセーフかな?という認識です。
SCC採用のもうひとつの理由として「MSXのKONAMI SCCメガロムマッパーと互換性のあるプログラムを記述をできるようにする」というものがあります。
SCC 以外の候補としてはPCエンジンの波形メモリ音源などもありました。
SCC と PCエンジンの波形メモリ音源 の主な違い(参考)
- 同時発音数:
- SCC: 5ch
- PCE: 6ch
- サラウンド
- SCC: モノラル
- PCE: ステレオ
- 波形テーブルサイズ:
- SCC: 8bits x 32bytes x 4tables
- PCE: 5bits x 32bytes x 6tables
- LFO:
- SCC: なし (サウンドドライバで力技実装するしかない)
- PCE: あり (HWでできるから簡単)
- Noise:
- SCC: なし
- PCE: あり (Ch0〜5それぞれでできる?)
機能的にはSCCの方がややシンプルな感じですが、シンプルだからこそ魔改造しやすいかな...ということでSCCをチョイス。
SCC エミュレータ実装
SCC エミュレータの実装はものすごくシンプルです。
FM音源だと比較的単純なOPLLでも1kステップ以上の規模になりますが、SCCなら100ステップ程度で組めるので、何ならQiita上にフルソースコードを掲載することもできます。
(SCCエミュレータのフル実装)
/**
* FAIRY COMPUTER SYSTEM 80 - SOUND CREATIVE CHIP Emulator
* -----------------------------------------------------------------------------
* The MIT License (MIT)
*
* Copyright (c) 2023 Yoji Suzuki.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
* -----------------------------------------------------------------------------
*/
#ifndef INCLUDE_SCC_HPP
#define INCLUDE_SCC_HPP
#include <string.h>
class SCC {
public:
struct Channel {
signed char waveforms[32];
int counter;
unsigned short period;
unsigned char volume;
unsigned char index;
};
struct Context {
struct Channel ch[5];
int sw;
} ctx;
SCC() {
this->reset();
}
void reset() { memset(&this->ctx, 0, sizeof(this->ctx)); }
inline unsigned char read(unsigned short addr) {
addr &= 0xFF;
return addr < 0x80 ? this->ctx.ch[addr / 0x20].waveforms[addr & 0x1F] : 0xFF;
}
inline void write(unsigned short addr, unsigned char value) {
addr &= 0xFF;
if (addr & 0x80) {
addr &= 0x3F;
if (addr < 0x0A) {
Channel* ch = &this->ctx.ch[addr >> 1];
ch->period = addr & 0x01 ? (ch->period & 0xff) | ((value & 0x0F) << 8) : (ch->period & 0xf00) | value;
} else if (addr < 0x0F) {
this->ctx.ch[addr - 0x0A].volume = value & 0x0F;
} else if (0x0F == addr) {
this->ctx.sw = value & 0x1F;
}
} else {
this->ctx.ch[addr >> 5].waveforms[addr & 0x1F] = (signed char)value;
}
}
inline void tick(short* left, short* right, unsigned int cycles) {
int mix[5];
int sw = this->ctx.sw;
for(int i = 0; i < 5; i++) {
Channel* ch = &this->ctx.ch[i];
if (ch->period) {
ch->counter += cycles;
while (0 <= ch->counter) {
ch->counter -= ch->period;
ch->index++;
ch->index &= 0x1F;
}
} else {
ch->index++;
ch->index &= 0x1F;
}
mix[i] = sw & 1 ? this->ctx.ch[4 == i ? 3 : i].waveforms[ch->index] * ch->volume : 0;
sw >>= 1;
}
int result = mix[0] + mix[1] + mix[2] + mix[3] + mix[4];
*left = this->to_short((*left) + result);
*right = this->to_short((*right) + result);
}
private:
inline short to_short(int i) {
if (32767 < i) return (short)32767;
if (i < -32768) return (short)-32768;
return (short)i;
}
};
#endif /* INCLUDE_SCC_HPP */
この単純な回路実装で以下のような豊かな表現力の音楽が再生できることにロマンを感じます。
厳密には、上記(スペースマンボウ)は SCC+PSG のあわせ技だと思いますが。(FCS80 も SCC+PSG の両方を搭載しているので同じ音楽の再生が理屈上は可能な筈です)
なお、SCCには別バージョン(スナッチャーやスナッチャーをスロットBに挿した状態の魔城伝説の曲)もあるようですが、チャンネル5の波形テーブルを独自定義できるかどうかの違いしかなく、個人的にチャンネル4と共用でも実用性の面での問題は無いと判断して古典的な方のSCCを採用しました。(アドレス
$98xx
を使いたかったという大きな理由もあります)
本書では、この素晴らしいSCC音源のドライバ開発のヒントとなる基本情報(仕様)やデバイス制御する上でのヒント情報を記します。
SCC の仕様
- 波形テーブル: 4つ
- 同時発音: 5チャネル
- チャネル 1〜3 は独立の波形テーブル 1〜3 を使用
- チャネル 4〜5 は1つの波形テーブル4を共用する
- ボリューム: 各チャネルで 0 (無音)〜 15 の範囲で指定可能
- 音程: PSG と同様 12 ビットで指定
I/O アクセス
FCS80 では以下のアドレスで SCC へアクセスします。
Address | r | w | Desciption |
---|---|---|---|
0x9800〜0x981F | o | o | 波形テーブル1 (チャネル1) |
0x9820〜0x983F | o | o | 波形テーブル2 (チャネル2) |
0x9840〜0x985F | o | o | 波形テーブル3 (チャネル3) |
0x9860〜0x987F | o | o | 波形テーブル4 (チャネル4/5) |
0x9880〜0x9881 | - | o | チャネル1の音程周波数(12bit) |
0x9880〜0x9881 | - | o | チャネル2の音程周波数(12bit) |
0x9882〜0x9883 | - | o | チャネル3の音程周波数(12bit) |
0x9884〜0x9885 | - | o | チャネル4の音程周波数(12bit) |
0x9886〜0x9887 | - | o | チャネル5の音程周波数(12bit) |
0x988A | - | o | チャネル1のボリューム(0〜15) |
0x988B | - | o | チャネル2のボリューム(0〜15) |
0x988C | - | o | チャネル3のボリューム(0〜15) |
0x988D | - | o | チャネル4のボリューム(0〜15) |
0x988E | - | o | チャネル5のボリューム(0〜15) |
0x988F | - | o | 各チャネルの発音/消音 |
偶然、KONAMI の SCC 対応の MSX ソフトのアドレスマップ(※スナッチャーを除く)と同じですね。(参考)
波形テーブル
SCC の波形テーブルは符号付き 8 ビットの整数(-128〜127)32バイトです。
以下に、波形テーブル1 に矩形波(PSGと同じ音)の波形データを設定するコーディング例を示します。
.SetCh1Square
LD HL, SquareTBL
LD DE, $9800
LD BC, 32
LDIR
RET
; 矩形波 (PSGと同じ)
SquareTBL: DB $80, $80, $80, $80, $80, $80, $80, $80
DB $80, $80, $80, $80, $80, $80, $80, $80
DB $7F, $7F, $7F, $7F, $7F, $7F, $7F, $7F
DB $7F, $7F, $7F, $7F, $7F, $7F, $7F, $7F
上記で $80
(-128) と $7F
(127) の個数を変えればデューティー比の変更が柔軟にできます。
PSG音源で鳴らすことができない三角波(笛やベースのような音)や2A03(ファミコン音源)でも鳴らすことができないノコギリ波(ストリングスのような音)の波形テーブルを作るのはもちろん、もっと複雑な波形を作ったり、再生中の波形を変化させるといった芸当もできそうです。
音程周波数
SCC の音程周波数(Frequency)の設定はPSGと全く同じです。
Tone | Oct-1 | Oct-2 | Oct-3 | Oct-4 | Oct-5 | Oct-6 | Oct-7 | Oct-8 |
---|---|---|---|---|---|---|---|---|
C | $D5D | $6AF | $357 | $1AC | $D6 | $6B | $35 | $1B |
C# | $C9C | $64E | $327 | $194 | $CA | $65 | $32 | $19 |
D | $BE7 | $5F4 | $2FA | $17D | $BE | $5F | $30 | $18 |
D# | $B3C | $59E | $2CF | $168 | $B4 | $5A | $2D | $16 |
E | $A9B | $54E | $2A7 | $153 | $AA | $55 | $2A | $15 |
F | $A02 | $501 | $281 | $140 | $A0 | $50 | $28 | $14 |
F# | $973 | $4BA | $25D | $12E | $97 | $4C | $26 | $13 |
G | $8EB | $476 | $23B | $11D | $8F | $47 | $24 | $12 |
G# | $86B | $436 | $21B | $10D | $87 | $43 | $22 | $11 |
A | $7F2 | $3F9 | $1FD | $FE | $7F | $40 | $20 | $10 |
A# | $780 | $3C0 | $1E0 | $F0 | $78 | $3C | $1E | $0F |
B | $714 | $38A | $1C5 | $E3 | $71 | $39 | $1C | $0E |
各チャネルの発音/消音
0x988F の bit-0 〜 bit-4 で発音(1)または消音(0)を設定できます。
bit-7 | bit-6 | bit-5 | bit-4 | bit-3 | bit-2 | bit-1 | bit-0 |
---|---|---|---|---|---|---|---|
- | - | - | Ch5 | Ch4 | Ch3 | Ch2 | Ch1 |
省略した機能(deformation register)
SCC には deformation register と呼ばれるものがあり、アドレス 0x98E0 (0x98E1〜0x98FFにミラー) に書き込むことで波形演算を変化させることができます。
ただし、この機能を使った市販ソフトは存在しないようなので、上述のエミュレータでは実装を省略してます。