Lovely 2SF Ripping Guide

This document explains how to rip music from a DS game to 2SF file, a Portable Sound Format for Nintendo DS.

If you are going to rip a game that uses standard DS sound format (SDAT), you do not need to read this guide. You can use VGMToolbox to convert directly DS ROM to 2SF. I also recommend you to create NCSF instead of 2SF.

If it’s not the case, it requires a manual ripping.

Prerequisites

You need to have a knowledge of some technical things:

  • Basics of Portable Sound Format and programming
  • ARM/THUMB assembly (DS hardware uses ARM946E-S and ARM7TDMI CPUs)
  • Some hardware architecture of Nintendo DS. Read GBATEK for the detailed hardware info.
  • 2SF format specifications - basically a 2SF is just a compressed ROM.

Tools

In this tutorial, I will use the following tools.

  • NO$GBA: High functionality emulator/debugger for GBA/NDS games. It’s a must-have tool.
  • DeSmuME: One of the best NDS emulator (I always use it for any casual purpose). It is open-source and has some good debugger features, since it is a standard DS emulator for tool-assisted speedrun community. I do not use it very much for 2SF ripping, however, I sometimes use it for RAM search, memory viewer and Lua scripting.
  • IDA Pro: User-friendly disassembler. It’s very expensive tool, but it will provide countless good functionality (graph view and xref search are my favorites). I cannot live without it.
  • Tinke: Viewer and editor for files of NDS games. I use it to view ROM header, export overlays, and explore the NitroFS filesystem.
  • 2SF Utilities
  • 2SF Player
  • NitroSDK (Official SDK) knowledges
    • Those libraries have a number of standard functions, such as OS_EnableInterrupts, FS_OpenFile, etc. If you have those knowledge, you will able to understand the things easier.
    • Could anyone provide a perfect FLIRT set of the SDK?
  • Hex editor: Everyone needs it for reverse-engineering work, right? You can choose your favorite editor.
    • 010 Editor: My best favorite editor! It’s not free, but very handy and so cute. It’s valuable to pay.
  • Text editor: Note everything you have found during the research, or you will lose them.

The whole strategy for 2SF ripping is quite similar to my generic PSF ripping strategy, however, there are some points to be noted.

  • DS hardware has a two CPUs, ARM946E-S and ARM7TDMI. ARM9 is a main CPU, and ARM7TDMI is a sub CPU that is mainly used for some I/O including sound registers (ARM9 cannot access those registers).
    • ARM9 need to communicate to ARM7TDMI, to manipulate sound registers. They use two short one-way queues named IPC FIFO, for a synchronous communication.
  • In DS era, a game may be written by C++, in object-oriented manner. That will make reverse-engineering more difficult, because of dynamic memory allocation and virtual function (function pointer in C).

Loading NDS ROM

Open your ROM in NO$GBA. That’s the first step of your adventure.

Open NDS ROM in IDA Pro

DS game has two entrypoints and code sections, that corresponds to ARM9 and ARM7TDMI. Those entrypoints and sizes are recorded in ROM header. Usually, they will be loaded into a different address of the same main RAM. (ARM9 code at 0x2000800, and ARM7TDMI code at 0x2380000, for example)

Music playback request must be demanded by ARM9, so we will start exploring the ARM9 code, and we will not see the ARM7 code.

To load a ROM into IDA Pro, there are several methods.

  • RECOMMENDED: Use Nintendo DS loader module for IDA Pro.
  • Manual loading: Run the game in an emulator (e.g. desmume) and export whole main RAM data into a file, then load it into IDA Pro as ARM code.
    • Pros: You can analyze code and data that dynamically loaded into RAM while your gameplay.
    • Cons: A number of tedious inputs, like as processor type, segment address and size, etc.

If you have installed IDA Signatures Pack, then open Signatures subview, and apply all “nintendo ds” sigunature files.

Next, you should create IO segment (4000000-400ffff). (Edit –> Segments –> Create new segment…)

f:id:loveemu:20170812100905p:plain

Then, set names for I/O registers by loading an IDAPython script, nds_arm9_regs.py. (File –> Script file…)

After reanalyzing program (Options –> General… –> Analysis –> Reanalyze program), the register names will be shown in the code.

f:id:loveemu:20170812103505p:plain

Load Overlay into IDA

You can skip this for the time being. You can do this later, when you really want to do.

Overlay is a delay-loadable code block. Programmers can save RAM usage by switching the overlay corresponding to the scene. Despite the music routine is quite common, some games might have the music routine in an overlay rather than main code (e.g. Soma Bringer).

You can export overlays via Tinke (it’s under the ftc directory). Also, overlays are recorded in Overlay Tables (OVT). You can find the load addresses for each overlays from there.

Here’s the steps for loading an overlay in IDA:

  1. Export an overlay to file. (Use Tinke)
  2. In IDA, click “File –> Load file –> Additional binary file…” and choose the overlay file
  3. Specify the load address and press OK (see the screenshot below)

f:id:loveemu:20170815001209p:plain

Note that I specify 0x200000 for loading the segment, since a paragraph is a 16 bytes unit. It’s meaning the address 0x200000.

First Step of IDA Analysis

The following things are optional, but I suggest you to do the first.

  1. Try identifying library functions by hand (if you know about NitroSDK functions)
    • BOOL FS_OpenFile(FSFile *p_file, const char *path) function could be a good starting point. You can find it from a random filename input (see Strings view).
  2. Find informative filenames and/or log messages from Strings view.
    • For instance, “/sounddata/sounddata.bin”, “[sound] Initialization completed normally”, “start: Memory_CreateHeap()” etc.
    • Those texts may be Japanese text (Shift_JIS encoded).
    • Jump to xref to operand, and give a good name to the function.
    • Sound driver code block might be located near the sound log message.

Explore the filesystem

You should view the filesystem and guess sound data is stored into what file. Tinke will show them in a friendly GUI.

f:id:loveemu:20170812103751p:plain

You can extract a file. You can also extract executable codes from ftc/arm9.bin and ftc/arm7.bin.

Guess The Sound Select Style

Since DS game has a build-in filesystem, we can find a code for opening a sound file easily, by finding a xref to the filename by IDA.

Note that opening a file is just a part of the sound code. Sound functions can be divided to some parts, like the following:

  1. Initialize the sound driver and I/O registers (sound and interrupts)
  2. Load the common data (it can be a part of initialization)
  3. Select a song
    • Load the song-specific data (e.g. sequence, instruments)
    • Make a request object for playing the song
  4. Send the request to ARM7TDMI from main loop function

How a game select a song? How the sound data is stored to the filesystem? It depends on the game. Let me show some examples.

SOUND SELECT STYLE 1: sprintf (Doki Doki Majou Shinpan’s CRI ADX driver)

Thanks: it’s mostly quoted from UNKNOWNFILE’s guide.

This game uses an algorithm to use a sprintf call before playing the ADX files. The sprintf values are thus:

RAM:020BF780 aSBgm_DD_adx    DCB "%s/BGM_%d%d.adx",0 ; DATA XREF: RAM:off_207E568
RAM:020BF790 a_dataSoundBgm  DCB "_Data/Sound/Bgm",0 ; DATA XREF: RAM:off_207E56C

At this point, we trace back to the calling function and we get this:

RAM:0207E534                 SUB     LR, R4, R0
RAM:0207E538                 LDR     R1, =aSBgm_DD_adx
RAM:0207E53C                 LDR     R2, =a_dataSoundBgm
RAM:0207E540                 ADD     R0, SP, #0x50+var_4C
RAM:0207E544                 STR     LR, [SP,#0x50+var_50]
RAM:0207E548                 ADD     R3, R12, R3,ASR#2
RAM:0207E54C                 BL      sub_20118C4
sub_20118C4(&var_4C, "%s/BGM_%d%d.adx", "_Data/Sound/Bgm", r12 + r3 / 4, r4 - r0);

The strings are loaded to r1 and r2, and the sound digit is loaded into r4. Therefore, we write the song value to r4.

// Solution 1: force the song number in the callee function
void selectSong(...) {
    r4 = 42; // set the song number here
    sub_20118C4(&var_4C, "%s/BGM_%d%d.adx", "_Data/Sound/Bgm", r12 + r3 / 4, r4 - r0);
}
// Solution 2: tweak the song number argument in the caller function (better style)
void selectSong(int r4, ...) {
    sub_20118C4(&var_4C, "%s/BGM_%d%d.adx", "_Data/Sound/Bgm", r12 + r3 / 4, r4 - r0);
}

void parent(...) {
    song = 42; // set the song number here
    selectSong(song);
}

SOUND SELECT STYLE 2: Register holds the sound value (Generic driver, Super Princess Peach)

Thanks: it’s mostly quoted from UNKNOWNFILE’s guide.

This sound select style could have used especially for an archived sound file, like SDAT.

Games using this sound select style are harder to rip as there is no obvious way to identify where the sound select value is actually given to the engine. A good place to start is to place breakpoints in functions following an initialization function which is obvious to locate (generic driver games all use a call to load a file such as sound_data.sdat).

A typical generic driver sound select looks like this:

ROM:02060BB8                 STMFD   SP!, {R4,R5,LR}
ROM:02060BBC                 SUB     SP, SP, #0xC
ROM:02060BC0                 MOV     R4, R1
ROM:02060BC4                 MOV     R5, R0
ROM:02060BC8                 MOV     R0, R4
ROM:02060BCC                 BL      sub_205F8D8
// C style
void sub_02060BB8(Foo *r0, Bar *r1) {
    // r0 = 0x02060BCC;
    // r1 = 0x02060BB8;
    sub_205F8D8(r0);
}
// C++ style
void Foo::sub_02060BB8(Bar *r1) {
    // r0 = this = 0x02060BCC;
    // r1 = 0x02060BB8;
    this->sub_205F8D8();
}

In this case, both r0 and r1 were pointers. They may point to the classes which holds a sound select value, or pre-loaded song sequence itself.

In some cases, the sound select value requires you to trace back and keep inserting values into a register until the music changes.

I will explain more hints on this style later, because it’s more difficult than the name-based style.

Identifying The Sound Select Value

Well yeah, we find that manual 2SF ripping is not very easy… Gargh! Nevermind! I’m Ret-2-go!

So the first goal is to know what sound select value is used to specify the song. My favorite starting point is finding a score pointer for the specific song playback. I use DeSmuME for this RAM searching.

f:id:loveemu:20131230222225g:plain

  1. Find a score data pointer address by DeSmuME’s RAM search
  2. Back to NO$GBA. Reset the game, set write breakpoint to the score pointer (set something like [2014514]!! in “Define Break/Condition” dialog), and find the code that writes the initial value there
  3. Backtrace the call tree (Run to Sub-Return), and…
    • Try to find the sound select value from the top-most function
    • Try to find a function which is called once per a song change
  4. Remember a few of song select values that correspond to very first songs (Logo, Title, Menu, etc.)
  5. Break at the song select code, then try overwriting the song select value and see whether it changes the song

Note: If you doubt “Run to Sub-Return” working properly, you can confirm the LR (r14) register at the beginning of the function.

I usually write up a simple song-value lookup table like below.

Song Value
Logo 0x1cb
Title 0x190
Menu 0x191

Rarely, despite you have hacked the correct function and the song select value, the game may get stuck. I experienced that with the title screen of Super Princess Peach. Unexpected combination of conditions might cause such an unfortunate glitch. (IPC FIFO overflow, for example?)

Installing The Driver Code

You somehow managed to change the song on debugger, but you lack some informations to write a clear 2SF driver code, at present. In particular:

  • Exact prerequisites for the song playback (such as initialization)
  • Where to put your 2SF driver code: a very earlier part of the game is most preferable

To investigate the prerequisites, I recommend to see what will disable the song playback, by NOP-out the function call nearby. I usually write a memo on the function call, like the following:

RAM:02011510 loc_2011510
RAM:02011510                 LDR             R0, [R4,#8] ; load song number
RAM:02011514                 LDR             R1, [R4,#0xC]
RAM:02011518                 MOV             R0, R0,LSL#16
RAM:0201151C                 MOV             R1, R1,LSL#24
RAM:02011520                 MOV             R0, R0,LSR#16
RAM:02011524                 MOV             R1, R1,ASR#24
RAM:02011528                 BL              sub_2014044 ; required for sound
RAM:0201152C                 LDR             R0, [R4,#0x10]
RAM:02011530                 ANDS            R0, R0, #0x200
RAM:02011534                 BEQ             loc_201153C
RAM:02011538                 BL              sub_20CA908 ; NOP-able

If a function is called so many times, you can disable by patching the callee itself.

RAM:02014044 sub_2014044
RAM:02014044                 BLX             LR      ; return immediately
RAM:02014048                                         ; remaining code will be ignored

At the same time, make your driver code and test inserting it to various locations (e.g. just after the song select code you look at, an earlier point in the main function, etc.) I usually write an assembly on my text editor, and paste it to NO$GBA by hand, line by line. (so clumsy!)

I will show my Super Princess Peach driver block below as an example.

.arm
    ; some basic setup...

loc_2000BDC:
    ; initialize sound
    bl #0x2014150

    ; disable vblank irq
    mov r0, #0
    bl #0x2093584

    ; bgm playback request
    ; ldr r0, [pc, #xx] or
    ; ldr r0, [sp, #xx]
    ldr r0, var_SongNumber
    mov r1, #0
    bl #0x2014044

    ; more setups
    mov r0, #1
    bl #0x20141dc

    ; enable vblank irq
    mov r0, #1
    bl #0x2093584

loc_MainLoop:
    ; main for sound
    bl #0x2014010

    ; wait for vblank
    blx #0x200072a

    b loc_MainLoop

var_SongNumber:
    dcd 0x190   ; the value will be patched by mini2sf
                    ; remember the address (offset on ROM, to be exact) of this variable, for the mini2sf creation

In addition, V-Blank wait function is:

.thumb_func

RAM:0200072A VBlankIntrWait
RAM:0200072A                 MOVS            R2, #0
RAM:0200072C                 SVC             5
RAM:0200072E                 BX              LR

Save the change to ROM file by using a hex editor. NO$GBA is good for quick check, but it probably does not have a function to save altered ROM.

Filling the unnecessary function calls with NOP, as far as you can, will allow you to optimize your 2sflib more tightly.

Make The 2SF Set

First, convert the patched ROM into 2SF.

Create .2sflib and .mini2sf files

rom2sf foo.nds

The command above will produce foo.2sf. Can the 2SF be played correctly? Good!

Next, rename the file to foo.2sflib and generate mini2sf.

# mini2sf <Base name> <Offset> <Size> <Count>
mini2sf foo 0x4c2c 4 873

Note: If you need to create mini2sfs which have more complicated parameter block, you will need to create parameter .bin files and batch convert them to mini2sfs, by using rom2sf with --load and --lib option. I personally use my private tool minigen for the flexible numbered .bin file creation.

Do they sound correctly? If so, it’s time to start optimizing your 2SF.

Optimization

I suggest you to run trim2sflib with a single mini2sf first, then convert the trimmed 2sflib file back to NDS ROM, and view the NitroROM File System by Tinke.

trim2sflib foo.2sflib foo-0000.mini2sf
2sf2rom -o foo.trimmed.nds foo.2sflib.trimmed

In most cases, Tinke will show some non sound-related filenames. This is because the DS file library touches those FAT entries chunk by chunk, when it searches the sound file. Unless it is really loaded, the file content must be filled with zero.

Give your final shot to the optimizer, if you think your work is ready for the final optimization.

trim2sflib foo.2sflib *.mini2sf

If you worry about the corrupted NitroROM file system, try modifying the FNT/FAT table by hex editor.

  1. Repair FNT table by copying the whole data from an unaltered ROM
  2. Fill FAT table with zero, except the files you really need (i.e. produce 0 byte files)

I hope this guide will help you in making your lovely rips!