Greetings to all the
This article is based on Github repo: ValleyBell/MidiConvertes and Mistydemeo/
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…
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
- 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
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
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
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 (
=== 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.
太强了
光速前排(
只是摸个鱼,,,
下次试试看分析fmp的格式吧。。
说到播放器的话,只知道安卓上有个 Droidsound-e,但是看起来作者并没有发布(支持 PMD 的)新版的源代码,所以。。
以及 FM3Extend 这玩意有点蛋疼