This is a technical analysis note of the sound engine of PS1 Dragon Quest VII.
DQVII uses a custom sound engine, developed by Heart Beat. PS1 Dragon Quest IV also uses the same sound engine.
Note: I used Japanese version of Dragon Quest VII (SLPM-86500)
See Also: Dragon Quest VII PSF set
DQVII CD has only 2 files, actually. One is named as "SLPM_865.00", it's the main PSX-EXE file. Sound program is a part of it. The other one is named as "HBD1PS1D.Q71". It's a large file archive. Sound data (sequences and instruments) are stored in the archive.
I made a utility called "psdq7rip", which is able to rip all sound files out from the archive. Over 7000+ files will be extracted. Sounds awful! In fact, most of them are duplicated. I strongly recommend to remove unwanted copies by using a cleanup utility. (say, FileHammer)
Sound file is designed as follows:
Offset | Size | Description |
---|---|---|
0x00 | 4 | SEQ size (could be 0) |
0x04 | 2 | SEQ id (msq_id) |
0x06 | 1 | Instrument bank count (usually 4) |
0x07 | 1 | Load position # (Normal BGM specifies 0; loads to constant address) |
0x08 | 4 | Reserved (probably not used) |
0x0c | 4 | Instrument bank #1 - PSX-ADPCM (aka VB) size (if 0, use preloaded bank) |
0x10 | 4 | Instrument bank #1 - Instrument attributes size |
0x14 | 2 | Instrument bank #1 - unknown |
0x16 | 2 | Instrument bank #1 - unknown |
0x18 | 12 | Instrument bank #2 - same as above |
0x24 | 12 | Instrument bank #3 - same as above (cannot be used) |
0x30 | 12 | Instrument bank #4 - same as above (cannot be used) |
0x3c | n | Data (SEQ, ADPCM, Instrument header) |
It can have both of sequences and instruments in one file. However, the game seems to load all waveforms (in a single file) before Enix logo, then, it loads a sequence file (Overture for example) which contains no waveforms.
Our goal: load 2 sound files without CD, then start music playback
- Sound loader function reads data from CD little by little. It sadly does not load the entire file into main memory at one time.
- Need to find a large free space to put waveforms. (432,800 bytes)
Function for loading a sound file is located at 0x80082598 (I named it as mopen_file). The function loads ADPCM to SPU (if present), then loads a SEQ (if present). The function takes only one argument, a pointer to a struct. It's something like a FILE*. It manages current file offset, number of bytes need to be transfered, number of bytes transfered in the last operation, buffer address, and so on.
Specifically, it is designed as follows:
Offset | Size | Description |
---|---|---|
0x00 | 4 | Pointer to a struct, the first argument of open_file |
0x04 | 4 | - |
0x08 | 4 | Pointer? |
0x0c | 4 | Pointer? |
0x10 | 4 | - |
0x14 | 4 | [O] Data buffer, file data will be stored to this buffer |
0x18 | 4 | [O] Number of bytes transfered in the last operation |
0x1c | 4 | [I] Number of bytes need to be transfered |
0x20 | 4 | Another buffer pointer? |
0x24 | 4 | Unknown |
0x28 | 4 | Unknown |
0x2c | 4 | - |
0x30 | 2 | - |
0x32 | 2 | - |
0x34 | 4 | - |
0x38 | 4 | - |
0x3c | 4 | Pointer? |
0x40 | 4 | Pointer? |
0x44 | 4 | - |
0x48 | 4 | - |
0x4c | 4 | - |
It is also passed to a CD reader function located at 0x800769CC. (I named it as mread_cd)
mopen_file reads data from CD little by little. It sadly does not load the entire file into main memory at one time.
mopen_file is a sort of subfunctions of the function located at 0x80076040. (I named it as open_file) mopen_file seems to be called if the file type (or whatever a 16bit value) is 0x13.
mopen_file takes only one argument, a pointer to a struct. It's something for specifying a file.
Specifically, it is designed as follows:
Offset | Size | Description |
---|---|---|
0x00 | 4 | Unknown |
0x04 | 2 | File type (0x13 for sound) |
0x06 | 2 | - |
0x08 | 4 | Offset? |
0x0c | 4 | Offset? |
0x10 | 4 | - |
0x14 | 4 | - |
0x18 | 4 | - |
0x1c | 4 | - |
0x20 | 4 | Physical file offset? |
0x24 | 4 | Physical file offset? |
0x28 | n | More members |
I decided to hack the original sound function before applying the PSF-o-Cycle driver code. It was just like a reprogramming rather than a ripping.
Changes:
- Modify mopen_file for PSF
- Change argument: $a0 is changed to a RAM address that points to the top of a sound file
- Use memcpy instead of mread_cd (both of them are not used for SEQ, since the driver does not need to copy it to a specific location)
- Minimize buffer size for DMA 4 from 2064 * 20 bytes to 2064 bytes (save memory space, and make the code simpler)
- Load SEQ from preloaded area directly, regardless of file header contents
- Always call mopenplay_seq after SEQ load (original driver do it only for normal BGM SEQ)
The structure is quite simple.
- dq7.psflib: music driver (altered SLPM_865.00) + instrument file
- dq7-xx.minipsf: sequence file
Instrument file and sequence file are located to constant address. The location is specified by PSF-o-Cycle driver code.
- Sequence starts from 0x80138000
- Instrument starts from 0x80180000
A sequence file can be converted to PSF1 by using bin2psf1. The tool is especially good for batch conversion.
bin2psf1 -r Japan filename.snd 0x80138000 0x8008DAC0 0x801FFFF0
You really do not need to care about this section.
- 80025300-800258E8: Tables for sequence interpretation
- 8002FA10-80036FCC: Sound programs (interrupt thread)
- 80082480-800849D4: Sound programs
- 80090E54-80094D14: PsyQ Sound functions
- 800B9D10-800BB6E7: RAM used by SPU library functions
Be careful, they can be wrong.
Most function names are unofficial. Some of them can be picked up from printf arguments.
Function name | Segment | Start | Length | Comments |
---|---|---|---|---|
mmain | TEXT | 8002FBFC | 00000428 | Callback registered by OpenEvent |
mmalloc_spu | TEXT | 80030F18 | 00000058 | |
mplay_seq | TEXT | 80031310 | 00000078 | $a0: SEQ address, $a1: 0x1200? |
mseq_midi_msg | TEXT | 800322CC | 00000190 | |
mseq_short_msg | TEXT | 8003245C | 00000108 | $a0: voice ptr, $a1: message byte |
mseq_control_change | TEXT | 80032564 | 00000324 | |
mseq_meta_event | TEXT | 80033418 | 00000044 | |
mseq_program_change | TEXT | 800335E8 | 00000110 | |
mentry_seq | TEXT | 800346B8 | 000001C4 | |
mopenplay_seq | TEXT | 80034988 | 000001BC | |
mopenplay_seq_fadein | TEXT | 80035684 | 0000009C | |
mpause_seq | TEXT | 800358F8 | 0000009C | |
mreopen_seq | TEXT | 80035CD4 | 00000070 | |
mdelete_seq | TEXT | 80035D44 | 00000048 | |
mopen_wave | TEXT | 800360E4 | 00000080 | |
main | TEXT | 8004EB30 | 000001F4 | Entry point |
mspu_transfer_sub | TEXT | 80074934 | 000000B0 | |
mspu_transfer_callback | TEXT | 80075BA4 | 00000098 | |
open_file | TEXT | 80076040 | 00000130 | $a0: file struct ptr, calls mopen_file |
mread_cd | TEXT | 800769CC | 0000005C | $a0: file reader struct ptr |
mspu_transfer | TEXT | 80076A28 | 00000020 | |
minit | TEXT | 80082480 | 00000118 | |
mopen_file | TEXT | 80082598 | 000007F0 | $a0: file reader struct ptr |
start | TEXT | 8008DAC0 | 00000034 | |
VSync | TEXT | 80095E24 | 00000178 | |
printf | TEXT | 800A0584 | 0000003C |
Address ranges are estimated roughly.
- 8002FA10-80036FCC: Sound programs
- 8008ED80-800A20AC: PsyQ Library functions
- 800B9D10-800BB749: RAM used by SPU library functions
- 800C0827-800C19D7: RAM used by graphic library functions
- 800C19D8-800F9C00: BSS, cleared by startup routine (Also, $gp points to the top)
- 800D25C0-800D80EF: Load area for BGM SEQ (23,344 bytes in maximum)
- 800D80F0-800DA0F7: Instrument attributes
- 801BEA38-801C8B77: Buffer for DMA 4 ADPCM transfer (2064 bytes * 20 sectors)