VGMTransコーディングメモ - 基礎・シーケンス変換の初歩

VGMTransはゲーム音楽MIDIやDLSに変換可能なGUIツールです。多種多様なフォーマットに対応しているのが本ツールの特長のひとつです。ソースコードには多様な状況に簡単に対応するための便利な機能が多く含まれています。

プログラマはアドオンを開発するような要領で、VGMTransのサポートファイル形式を拡張することができます。MIDIファイル出力、DLSファイル出力、GUI等の機能はすべてVGMTransに備えられています。プログラマはそれらのおかげで本当に必要なコーディングだけに注力できます。そして、ツールの利便性を向上させ、不要なコーディング時間を短縮することができます。

しかしながら、想定される入力が多様であるため、VGMTransの構造も幾分複雑です。本書の目的は、VGMTransの基本的構造を記載し、より多くの開発者によるVGMTransの拡張を可能にすることにあります。

本書は VGMTrans_src_and_bin_9_29_09 を対象としており、内容が以降のバージョンに適合することは保証しません。

ファイル・コンテナ類

主要な基底クラス類はおよそ下記の通りです。

Loader *

Format *
  L Scanner *
  L Matcher

VGMColl * (→MIDI and DLS)
  L VGMSeq * (→MIDI)
    L SeqTrack *
      L SeqEvent
  L VGMInstrSet * (→ DLS)
    L VGMSampColl * (→ WAVs)
      L VGMSamp * (→ WAV)
    L VGMInstr *
      L VGMRgn

 * You may need to make a derived class.

それぞれのクラスの役割は下記の通り。必要に応じて派生クラスを作成して使用します。

Loader
アーカイブされた入力ファイルを展開する。既存にPSF1Loaderなどがある。
Format
すべての頂点。主にScannerとMatcherを集約したクラス。新しいフォーマットに対応するには派生クラスの作成が必須。
Scanner
入力ファイルから対象シーケンスやインストゥルメントセットを検索するクラス。派生クラスの作成が必須。
Matcher
適切なVGMSeqとVGMInstrSetを関連付けてVGMCollを作成するクラス。主要なMatcherは用意済みのため、派生クラスの作成は通常必要ない*1
VGMColl
VGMSeqとVGMInstrSetを集約したクラス。楽曲の譜面と音色の組。
VGMSeq
楽曲の譜面。1つ以上のSeqTrackを持つ。譜面は楽曲の要なので、派生クラスの作成が実質必須。
SeqTrack
トラック。1つ以上のSeqEventを持つ。フォーマット固有の状態変数を保持するために派生クラスを作成することが多い。
SeqEvent
イベント。多くの派生クラスが SeqEvent.cpp に定義されており、SeqTrack を通じて利用可能である。通常新しい定義を増やす必要はない。
VGMInstrSet
楽曲のインストゥルメントセット。VGMSampCollとVGMInstrを集約したクラス。
VGMSampColl
波形サンプルの集合体。複数のVGMSampを持つ。波形アーカイブもしくはテーブルに相当する。
VGMSamp
単一の波形サンプル。時にはPCM、時にはADPCM、時にはBRRなどであり得る。
VGMInstr
単一の楽器。単一のバンク番号、楽器番号、及び1つ以上のVGMRgnを集約したクラス*2
VGMRgn
単一の音色定義。音量、ピッチ、ADSR、キーレンジ、ベロシティレンジ等の情報を持つ。

また、これ以外の要素を新しく必要とする場合、VGMContainerItem の派生クラスとして作成することができます(例:NinSnesSection)。

譜面をMIDIデータに変換したいときは、少なくとも Format、Scanner、及び VGMSeq の派生クラスの作成を要します。

Format、Scanner、及び VGMSeq の派生クラスを作る

Formatの派生クラスの作成は簡単です。

#pragma once
#include "Format.h"
#include "Root.h"
#include "VGMColl.h"
#include "XyzScanner.h"

BEGIN_FORMAT(Xyz)
	USING_SCANNER(XyzScanner)
END_FORMAT()
  • USING_SCANNER で Scanner を関連付けます。
  • 必要であれば USING_MATCHER, USING_MATCHER_WITH_ARG で Matcher を関連付けることができます。
  • 必要であれば USING_COLL で独自の VGMColl 派生クラスを関連付けることができます。

単純な Scanner は下記のようなシンプルな Scan メソッドを持ちます(ファイル全体に対する線形探索の例)。

#include "stdafx.h"
#include "XyzScanner.h"
#include "XyzSeq.h"

XyzScanner::XyzScanner(void)
{
}

XyzScanner::~XyzScanner(void)
{
}

void XyzScanner::Scan(RawFile* file, void* info)
{
	SearchForXyzSeq(file);
	return;
}

#define SIGNATURE_LEN	4
void XyzScanner::SearchForXyzSeq (RawFile* file)
{
	const BYTE SIGNATURE[SIGNATURE_LEN] = { 'X', 'Y', 'Z', 0 };

	UINT nFileLength = file->size();
	for (UINT offset = 0; offset + SIGNATURE_LEN < nFileLength; offset++)
	{
		bool signatureMatched = true;
		for (UINT i = 0; i < SIGNATURE_LEN; i++)
		{
			if ((*file)[offset + i] != SIGNATURE[i])
			{
				signatureMatched = false;
				break;
			}
		}

		if (signatureMatched)
		{
			XyzSeq* NewXyzSeq = new XyzSeq(file, offset);
			NewXyzSeq->LoadVGMFile();
		}
	}
}

Format 及び Scanner を作成したら、Root.cpp の VGMRoot::Init() で登録を行います*3

// initializes the VGMRoot class by pushing every VGMScanner and
// VGMLoader onto the vectors.
bool VGMRoot::Init(void)
{
	UI_SetRootPtr(&pRoot);

	//load all the formats
	AddScanner("NDS");
	AddScanner("Akao");
	AddScanner("FFT");
	AddScanner("HOSA");
	AddScanner("SquarePS2");
	AddScanner("SonyPS2");
	AddScanner("TriAcePS1");
	AddScanner("Xyz");

	//load all the... uh, loaders
	AddLoader<PSF1Loader>();
	AddLoader<PSF2Loader>();
	AddLoader<SPCLoader>();
	AddLoader<MAMELoader>();

	return true;
}

VGMSeq の派生に関してはいろいろな形がありますが、シンプルな一例はこのようになりそうです。主に GetHeaderInfo と ReadEvent が書ければOKです。

#include "stdafx.h"
#include "XyzSeq.h"

DECLARE_FORMAT(Xyz);

XyzSeq::XyzSeq(RawFile* file, ULONG offset)
: VGMSeq(XyzFormat::name, file, offset)
{
	//vgmfile  <- file
	//dwOffset <- theOffset

	//UseReverb();
	//UseLinearAmplitudeScale();
	//AlwaysWriteInitialVol(100);
	//AlwaysWriteInitialExpression(127);
	//AlwaysWriteInitialPitchBendRange(2, 0);
}

XyzSeq::~XyzSeq(void)
{
}

int XyzSeq::GetHeaderInfo(void)
{
	SetPPQN(GetShort(dwOffset));
	name = L"Xyz Seq";

	// Tracks pointer can be read here,
	// but I guess GetTrackPointers is the true place for it.

	return true;		//successful
}

int XyzSeq::GetTrackPointers(void)
{
	ULONG seqLength = 2 + 2 * 8; // header size
	for (int i = 0; i < 8; i++)
	{
		USHORT trackOffset = GetShort(dwOffset + 2 + 0 + i * 4);
		USHORT trackSize = GetShort(dwOffset + 2 + 2 + i * 4);
		if (trackOffset != 0)
		{
			nNumTracks++;					//well then, we might as well say it exists
			XyzTrack* newXyzTrack = new XyzTrack(this, trackOffset, trackSize);
			//newXyzTrack->xxxx = GetShort(dwOffset + xxxx); // set additional parameters here if needed
			aTracks.push_back(newXyzTrack);

			seqLength += trackSize;
		}
	}
	unLength = seqLength;

	return true;
}

XyzTrack::XyzTrack(XyzSeq* parentFile, long offset, long length)
: SeqTrack(parentFile, offset, length)
{
	//SeqTrack:
	//	pMidiTrack = NULL;
	//	vol = 100;
	//	expression = 127;
	//	prevPan = 64;
	//	channelGroup = 0;
	//	transpose = 0;
	//	cDrumNote = -1;
	//	cKeyCorrection = 0;
	//	bDetermineTrackLengthEventByEvent = false;

	// curOffset <- offset
}

int XyzTrack::LoadTrack(int trackNum, ULONG stopOffset)
{
	// Initialize track variables
	return true;
}

int XyzTrack::ReadEvent(void)
{
	ULONG beginOffset = curOffset;
	BYTE status_byte = GetByte(curOffset++);

	if (status_byte < 0x80) //then it's a note on event
	{
		BYTE vel = GetByte(curOffset++);
		dur = ReadVarLen(curOffset);//GetByte(curOffset++);
		AddNoteByDur(beginOffset, curOffset-beginOffset, status_byte, vel, dur);
		if(noteWithDelta)
		{
			AddDelta(dur);
		}
	}
	else switch (status_byte)
	{
	case 0x80:
		dur = ReadVarLen(curOffset);
		AddRest(beginOffset, curOffset-beginOffset, dur);
		break;

	case 0x81:
		{
			BYTE newProg = (BYTE) ReadVarLen(curOffset);
			AddProgramChange(beginOffset, curOffset-beginOffset, newProg);
		}
		break;

	case 0x95:
		loopReturnOffset = curOffset + 2;
		AddGenericEvent(beginOffset, curOffset+2-beginOffset, L"Jump", BG_CLR_YELLOW);
		curOffset = GetByte(curOffset) + (GetByte(curOffset+1)<<8) + parentSeq->dwOffset;
		break;

	case 0xC0:
		{
			BYTE pan = GetByte(curOffset++);
			AddPan(beginOffset, curOffset-beginOffset, pan);
		}
		break;

	case 0xC1:
		vol = GetByte(curOffset++);
		AddVol(beginOffset, curOffset-beginOffset, vol);
		break;

	case 0xC3:
		{
			int transpose = (signed) GetByte(curOffset++);
			AddTranspose(beginOffset, curOffset-beginOffset, transpose);
		}
		break;

	case 0xC4:
		{
			SHORT bend = (signed) GetByte(curOffset++) * 64;
			AddPitchBend(beginOffset, curOffset-beginOffset, bend);
		}
		break;

	case 0xC5:
		{
			BYTE semitones = GetByte(curOffset++);
			AddPitchBendRange(beginOffset, curOffset-beginOffset, semitones);
		}
		break;

	case 0xC9:
		curOffset++;
		AddUnknown(beginOffset, curOffset-beginOffset, L"C9");
		break;

	case 0xCA:
		{
			BYTE amount = GetByte(curOffset++);
			AddModulation(beginOffset, curOffset-beginOffset, amount, L"Modulation Depth");
		}
		break;

	case 0xCB:
		curOffset++;
		AddGenericEvent(beginOffset, curOffset-beginOffset, L"Modulation Speed", BG_CLR_CYAN);
		break;

	case 0xCE:
		{
			bool bPortOn = GetByte(curOffset++);
			AddPortamento(beginOffset, curOffset-beginOffset, bPortOn);
		}
		break;

	case 0xCF:
		{
			BYTE portTime = GetByte(curOffset++);
			AddPortamentoTime(beginOffset, curOffset-beginOffset, portTime);
		}
		break;

	case 0xD5:
		{
			BYTE expression = GetByte(curOffset++);
			AddExpression(beginOffset, curOffset-beginOffset, expression);
		}
		break;

	case 0xE1:
		{
			USHORT bpm = GetShort(curOffset);
			curOffset += 2;
			AddTempoBPM(beginOffset, curOffset-beginOffset, bpm);
		}
		break;

	case 0xFD:
		AddGenericEvent(beginOffset, curOffset-beginOffset, L"Return", BG_CLR_YELLOW);
		curOffset = loopReturnOffset;
		break;

	case 0xFF:
		AddEndOfTrack(beginOffset, curOffset-beginOffset);
		return false;

	default:
		AddUnknown(beginOffset, curOffset-beginOffset);
		return false; // abort
	}
	return true;
}

とにかく、できることは多岐にわたります。大量の既存のソースコードには、私たちが新しく実現したいことに似た何かを含むものがあるはずです。探しだして参考にしましょう。

*1:ちなみに、Matcherを使用しなくてもScanner内部等でVGMCollを組み立てることは可能。

*2:キースプリットされたインストゥルメント、リズムキットは2つ以上のVGMRgnを持つ

*3:一見すると Format は無関係ですが、実際には AddScanner() の内部で処理されています。