[OPNA] An Analysis of The P.M.D. Music Data Format (Mostly the "Rhythm" Section)

Greetings to all the new comers.

This article is based on Github repo: ValleyBell/MidiConvertes and Mistydemeo/Pmdmini. Special thanks to オップナー2608 from PC-9800 Series Central Discord Channel for helping me.

Meh... a detailed description of this little project will be covered in a future article (If I actually would do). Basically, I'm trying to make a portable music player / midi synth based around YM2608. I expect this project to take forever to complete...orz

Let's get our hands dirty quick!

I started my investigation on this legendary sound system by checking out all the PC software available out there while listening to my favourite PC-98 Touhou arrangement: 神へ捧げる魂 ~ Highly Responsive to Prayers ( a.k.a. Th01_04 ). Here is a list of useful tools (all of which can be found easily on the internet):

  • Locale Emulator: Resolves Shift-JIS display issues on non-Japanese OSes.
  • FMPMDE ( together with PMDWin.dll and WinFMP.dll ): The Real-time Music Interpreter. Use this program to listen, toggle the channels on and off, and see the effects of your modifications.
  • drum_samples.zip: You can find this file on hoot's website. It contains all the drum samples inside YM2608's internal sample memory in WAV format. Without this zip, FMPMDE won't generate any drum sound (The effect can be observed by toggling the "SSG+RHY" switch).
  • MC.exe: The MML->PMD Compiler.
  • PMDRC: The PMD->MML Decompiler.
  • PC-98 Audio Rips.rar: Contains all the PMD files extracted from PC-98 Touhou games. You can also extract these files by yourself. (Detailed instructions can be found here).
  • Any HEX editor, such as Sublime Text + HexViewer Plugin.

At first, I thought I would be quite happy working with decompiled PMD files. However, it's not long before I found out that PMDRC doesn't bother with the rhythm sections. A track without drum is unlistenable; I'm forced to get a full understanding of PMD's file structure to continue.

For this reason, I attempted to understand the binary content of Th01_04.M using a HEX viewer. Unfortunately, there aren't many sources out there for me to get enough information. User オップナー2608 offered me this link: 
https://raw.githubusercontent.com/ValleyBell/MidiConverters/master/pmd_SeqFormat.txt, which is a detailed description of the PMD data format. The author of this document got his resources by laboriously reading the source code of  PMDwin.dll, whose author got his information from somewhere else in the PC-98 universe back in the 00s. Hats off to them.

I compared the description it against the MML file decompiled using PMDRC, sure enough, they match perfectly.

The text file says:

Header
------
1 byte - file version (can be 00..0F, usually 00 for .M/.M2, must be FF for FM Towns)

11*2 bytes - track pointers
	0-5: FM 1-6
	6-8: PSG 1-3
	9: OPNA ADPCM B
	10: OPNA Rhythm
2 bytes - Rhythm Subroutine Table
2 bytes - FM instrument pointer (also used for getting additional file pointers)

x bytes - sequence data

x*2 bytes - rhythm subroutine pointers
x bytes - rhythm subroutine data

2 bytes - extra data pointer
1 byte - extra data type (40..4F)
1 byte - must be FE

x bytes - FM instruments
x bytes - PSG envelopes

extra data pointers:
2 bytes - PPZ file name pointer (FM Towns) [type 48+]
2 bytes - PPS file name pointer [type 42+]
2 bytes - PPC/P86 file name pointer

Let's take a look at Th01_04.M. Here is the address table:

            0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F
00000000:  00 1a 00 55 01 9d 01 e5 01 2f 03 ef 03 5d 05 2f  
00000010:  06 7f 07 86 07 8d 07 d3 07 8a 08 ...

Keep in mind we're dealing with little endian (It's natural to think of, since PMD is highly optimized for the x86 architecture). A human-friendly version looks like this:

Data  | Comment            | Target Address
------+--------------------+--------------
00 		VERSION IDENTIFIER
1A 00	FM1 SEQUENCE DATA	@ 001Ah
55 01	FM2 SEQUENCE DATA	@ 0155h
9D 01	FM3 SEQUENCE DATA	@ 019Dh
E5 01	FM4 SEQUENCE DATA	@ 01E5h
2F 03	FM5 SEQUENCE DATA	@ 032Fh
EF 03	FM6 SEQUENCE DATA	@ 03EFh
5D 05	PSG1 SEQUENCE DATA	@ 055Dh
2F 06	PSG2 SEQUENCE DATA	@ 062Fh
7F 07	PSG3 SEQUENCE DATA	@ 077Fh
86 07	ADPCM DATA			@ 0786h
8D 07	RHYTHM SEQUENCE		@ 078Dh
D3 07	RHYTHM TABLE		@ 07D3h
8A 08	FM INSTRUMENT TABLE	@ 088Ah

Another important discrepancy to notice is that all of the file's data is shifted one byte forward, which means you should automatically add 1 to each of the encoded addresses. 

Now, since I want to focus on the rhythm section, I'll omit everything else in between. That text document does a great job explaining the FM and SSG channels. 

Side note: ZUN never uses SSG3 and ADPCM in his games. These channels are nearly empty, with just a few bytes of parameter settings:

=== SSG3
CA 01 
BB 01 
B2 FD 
80

=== ADPCM
CA 01 
BB 01 
B2 FD 
80

Now, let's take a look at the first section of the rhythm channel: the Rhythm Sequence Table, also known as channel "K" in the MML terminologies.

            0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F
00000780:                                            f6 f9
00000790:  93 07 00 f8 07 07 8f 07 01 f9 9d 07 02 f8 07 07
000007a0:  99 07 03 f9 a7 07 00 f8 07 07 a3 07 01 f9 b1 07
000007b0:  02 f8 07 07 ad 07 03 f9 bb 07 02 f8 07 07 b7 07
000007c0:  04 f9 ce 07 f9 c8 07 05 f8 07 07 c4 07 06 f8 02
000007d0:  02 c1 07 80                                    

Now we're able to translate this according to that text document

ADDR   |COMMAND            |COMMENT
-------+-------------------+--------------------------------------------------------------------
078E	F6 					Master Loop Start

078F	F9 93 07			Loop Start, loop end @ 0793h
0792		00				Execute Pattern #00
0793	F8 07 07 8F 07 		Loop End, 7 times, space for loop counter = 7, jump to offset 0791h

0798	01					Execute Pattern #01

0799	F9 9D 07			Loop Start, loop end @ 079Dh
079C		02				Execute Pattern #02
079D	F8 07 07 99 07		Loop End, 7 times, space for loop counter = 7, jump to offset 079Bh

07A2	03					Execute Pattern #03

07A3	F9 A7 07			Loop Start, loop end @ 07A7h
07A6		00				Execute Pattern #00
07A7	F8 07 07 A3 07		Loop End, 7 times, space for loop counter = 7, jump to offset 07A5h

07AC	01					Execute Pattern #01

07AD	F9 B1 07 			Loop Start, loop end @ 07B1h
07B0		02 				Execute Pattern #02
07B1	F8 07 07 AD 07		Loop End, 7 times, space for loop counter = 7, jump to offset 07AFh

07B6	03					Execute Pattern #03

07B7	F9 BB 07			Loop Start, loop end @ 07BBh
07BA		02 				Execute Pattern #02
07BB	F8 07 07 B7 07		Loop End, 7 times, space for loop counter = 7, jump to offset 07B9h

07C0	04					Execute Pattern #04

07C1	F9 CE 07			Loop Start, loop end @ 07CEh
07C4		F9 C8 07		Loop Start, loop end @ 07C8h
07C7			05 			Execute Pattern #05
07C8		F8 07 07 C4 07	Loop End, 7 times, space for loop counter = 7, jump to offset 07C6h
07CD		06				Execute Pattern #06
07CE	F8 02 02 C1 07 		Loop End, 2 times, space for loop counter = 2, jump to offset 07C3h

07D3	80					Track End

The next section is an address lookup table, which contains the absolute address for each "rhythm patterns". The "patterns" are stored in the section next to it:

            0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F
000007d0:              e1 07 f0 07 02 08 13 08 30 08 50 08
000007e0:  61 08 e8 36 80 81 18 80 81 18 80 81 18 80 81 18
000007f0:  ff e8 36 80 81 18 80 81 18 80 81 18 80 81 0c 81
00000800:  00 0c ff e8 36 f9 0e 08 80 81 0c 81 00 0c f8 04
00000810:  04 05 08 ff e8 36 f9 1f 08 80 81 0c 81 00 0c f8
00000820:  03 03 16 08 80 82 06 80 82 06 80 82 06 80 82 06
00000830:  ff e8 38 f9 4b 08 82 02 06 80 01 06 80 01 06 82
00000840:  02 06 80 01 06 80 01 06 82 02 0c f8 02 02 33 08
00000850:  ff e8 36 f9 5c 08 80 81 0c 81 02 0c f8 04 04 53
00000860:  08 ff e8 36 f9 6d 08 80 81 0c 81 02 0c f8 03 03
00000870:  64 08 80 82 06 80 82 06 80 82 06 80 82 06 ff 00
00000880:  15 00 00 00 15 00 00
=== RHYTHM PATTERN ADDRESS LUT
E1 07
F0 07 
02 08 
13 08
30 08
50 08
61 08

=== RHYTHM PATTERNS
バス			Bass
スネア		Snare
タム			Tam
リム			Rim
クラップ		Clap
Cハイハット		C-HighHat
Oハイハット		O-HighHat
シンバル		Cymbal
RIDEシンバル	Ride Cymbal
(A Japanese Name LUT for human readers ;-)

E8 36			#0
80 81 18		バス, Cハイハット
80 81 18		..
80 81 18		..
80 81 18		..
FF				Return

E8 36			#1
80 81 18		バス, Cハイハット
80 81 18		..
80 81 18		..
80 81 0C		..
81 00 0C		Oハイハット
FF				Return

E8 36 			#2
F9 0E 08		Loop start
	80 81 0C	バス, Cハイハット
	81 00 0C	Oハイハット
F8 04 04 05 08	Loop end, 4 times
FF				Return 


E8 36			#3
F9 1F 08		Loop start
	80 81 0C	バス, Cハイハット
	81 00 0C	Oハイハット
F8 03 03 16 08	Loop end, 3 times
80 82 06		スネア, Cハイハット
80 82 06		..
80 82 06		..
80 82 06		..
FF				Return

E8 38			#4
F9 4B 08		Loop start
	82 02 06	シンバル, スネア
	80 01 06	バス
	80 01 06	..
	82 02 06	シンバル, スネア
	80 01 06	バス
	80 01 06	..
	82 02 0C	シンバル, スネア
F8 02 02 33 08	Loop end, 2 times
FF				Return

E8 36			#5
F9 5C 08		Loop start
	80 81 0C	バス, Cハイハット
	81 02 0C	Oハイハット, スネア
F8 04 04 53 08	Loop end, 4 times
FF				Return

E8 36			#6
F9 6D 08		Loop start
	80 81 0C	バス, Cハイハット
	81 02 0C	Oハイハット, スネア
F8 03 03 64 08	Loop end, 3 times
80 82 06		Cハイハット, スネア
80 82 06		..
80 82 06		..
80 82 06		..
FF

00 15 00 00 00 15 00 00 (Unknown Data, probably space filler)

You notices that I've attached what "rhythm" it actually is in Japanese. At first, unfortunately, the only thing I can get from the text file is:

80-BF bb ll - (command * 0x100 + bb) & 0x3FFF -> some ID/mask, length ll

What the heck "some ID/mask" is actually? In order to find an answer, I had to read the source code of PMDwin.dll. Luckily enough, all the primary logic of that program is stuffed into a single file.

=== RHYTHM COMMAND FORMAT: "Command Argument Duration"
	The "Command" starts at 0x80
	kshot_dat = ((CMD << 8) + ARG) & 0x3fff;
CMD:	1ccc_cccc
ARG:	          aaaa_aaaa
		00cc_cccc_aaaa_aaaa
		       |10        |0

	void OPM::SetReg(uint addr, uint data)
	
	Each of the 10 LSBs in "kshot_dat" corresponds to one sound, if a bit "cl" is set:
	opna.SetReg(rhydat[cl][0], rhydat[cl][1]);
	Find the corresponding PAN and VOLUME data from a lookup table, send it to the hardware registers.

	If that sound contains a "dump" flag, make a "HH" sound, else just make the sound.
	(This logic is kinda weird, probably has something to do with making correct sound when "maskon()" or "maskoff()" is called)
	
=== RHYTHM REGISTER:
				
			 |RIM|TOM|HH |TOP|SD |BD |
     |D7 |D6 |D5 |D4 |D3 |D2 |D1 |D0 |
0x10 |DM | X |        RKON           |
0x11 |   X   |         RTL           |
0x18 |   |   |    |                  |
0x19 |   |   |    |                  |
0x1A | L | R |    |        IL        |
0x1B |   |   |    |                  |
0x1C |   |   |    |                  |
0x1D |   |   |    |                  |

=== PMDWIN RHYTHM LUT
const int rhydat[][3] = {
	//	PT,PAN/VOLUME,KEYON
	{	0x18, 0xdf, 00000001b	},	//	バス
	{	0x19, 0xdf, 00000010b	},	//	スネア
	{	0x1c, 0x5f, 00010000b	},	//	タム[LOW]
	{	0x1c, 0xdf, 00010000b	},	//	タム[MID]
	{	0x1c, 0x9f, 00010000b	},	//	タム[HIGH]
	{	0x1d, 0xd3, 00100000b	},	//	リム
	{	0x19, 0xdf, 00000010b	},	//	クラップ
	{	0x1b, 0x9c, 10001000b	},	//	Cハイハット
	{	0x1a, 0x9d, 00000100b	},	//	Oハイハット
	{	0x1a, 0xdf, 00000100b	},	//	シンバル
	{	0x1a, 0x5e, 00000100b	}	//	RIDEシンバル
};

Phew... That's all I need to know for now.

The next 4 bytes are address markers for the Extra Data (Title, Composer, etc...).

5E 09 	Extra Data Pointer: 095Eh
48 		Type: PPZ file name pointer (FM Towns) [type 48+]
FE

Following this, comes the FM Instrument Definition Table. All the parameters of the FM synth chip can be tweaked by the programmer. I won't go into much detail for this section (maybe reserved for a future article?) If you're interested in it, please read the original YM2608 datasheet and Yamaha FM synth chip product line application manuals.

=== FM INSTRUMENT TABLE

0C				#12
3C 76 74 32 28 
23 00 00 15 15 
18 18 04 04 8C 
8C 02 02 04 04 
24 24 26 26 3C

17				#23
34 72 34 74 1E 
1F 00 00 DF DF 
1F 1F 00 00 02 
02 00 00 00 00 
02 02 37 37 3C 

2B 				#43
02 04 08 04 1C 
00 00 00 1F 1F 
1F 1F 00 0E 0E 
0E 00 03 03 03 
00 3F 3F 3F 3D 

B5 				#181
78 38 74 34 1B 
1E 00 00 1F 1F 
1F 1F 00 00 10 
10 00 00 00 00 
00 00 28 28 3C 


00 FF 00 00 00	(Unknown Data)

The Extra Data is also leaded by an address table:

=== EXTRA DATA

08F8h	90 5F 82 D6 95 F9 82 B0 82 E9 8D B0 81 40 81 60 20 48 69 67 68 6C 79 20 52 65 73 70 6F 6E 73 69 76 65 20 74 6F 20 50 72 61 79 65 72 73 00

0926h	82 79 82 74 82 6D 81 69 91 BE 93 63 81 6A 00
0935h	82 79 82 74 82 6D 81 69 91 BE 93 63 81 6A 00
0943h	E8 CB 88 D9 93 60 82 CC 8B C8 81 42 93 C1 95 CA 83 41 83 8C 83 93 83 57 94 C5 00

=== EXTRA DATA TABLE

F4 08	
F5 08
F6 08 
F7 08 
25 09 
34 09 
43 09 
00 00

Copy the ASCII data displayed in your HEX viewer, paste it into a new text file, then find a way to change its encoding to Shift-JIS. You get this:

神へ捧げる魂 ~ Highly Responsive to Prayers ZUN(太田) ZUN(太田) 靈異伝の曲。特別アレンジ版 

Yep, here are the title, composer, arranger and comment data.

That's pretty much it. Hope it can be helpful.

4 thoughts on “[OPNA] An Analysis of The P.M.D. Music Data Format (Mostly the "Rhythm" Section)”

  1. 说到播放器的话,只知道安卓上有个 Droidsound-e,但是看起来作者并没有发布(支持 PMD 的)新版的源代码,所以。。
    以及 FM3Extend 这玩意有点蛋疼

Leave a Reply to yksoft1 Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.