Elite Beat Ouendan: Music LIVES!
Elite Beat Ouendan: Music LIVES! is a Windows port of Elite Beat Agents and Osu! Tatakae! Ouendan 1 and 2 with controller support and display options.
- Type: PC port
- Game: Elite Beat Agents
- Runs on Windows
- By Behon
- Latest release v1.0.0-rc1, 2026-11-04
- Source: https://github.com/BehonArt/ebaouendan_musiclives
README
Elite Beat Ouendan: Music LIVES (eba-recomp)
Elite Beat Agents, Osu! Tatakae! Ouendan and Moero! Nekketsu Rhythm Damashii Osu! Tatakae! Ouendan 2 on Windows, recompiled from their own code to native PC programs, with a launcher, Tap mode for controllers (rumble, DualSense triggers), display and look options, assists and No-Fail, all three games alike.
You need your own cartridge dumps. Nothing of the games is in this
repository or its releases: the launcher asks for each game's .nds dump
(EBA USA, Ouendan 1 and 2 Japan) and checks it. The release zip is on the
Releases page: unpack it anywhere, run eba-recomp.exe,
point each game at its dump, play. Windows 10 or 11, 64-bit, a graphics
driver with OpenGL 4.5. The README in the zip covers controls and options.
The project's own code is MIT (LICENSE). Built on
ndskit (a static recompiler and
runtime for the Nintendo DS, MIT), SDL3, Dear ImGui and rcheevos. Not
affiliated with Nintendo or iNiS.
Everything below is the developer's notes.
eba-recomp
Static recompilation of Elite Beat Agents (USA, AOSE), Osu! Tatakae!
Ouendan (Japan, AOSJ) and Moero! Nekketsu Rhythm Damashii (Japan, AO2J) to
native PC code with ndskit
(submodule at ndskit/). The ndskit submodule needs the patches in
patches/ applied before building (see "Local ndskit patches").
Status (2026-10-22)
All three games boot headless with no interpreted functions, through the title, menus, stage map, a stage's intro comic, gameplay and the fail screen, with the real saves loaded and the music in the WAV.
| EBA | OE1 | OE2 | |
|---|---|---|---|
| ARM9 functions | 4,517 (6 refuse passes) | 3,226 | 4,086 program + 1,191 in 7 overlays |
| ARM7 functions | 1,026 | 1,004 | 1,026 (EBA's ARM7) |
| unresolved calls / jumps (ARM9 program) | 841 / 23 | 437 / 3 | 691 / 15 |
| self-test vectors, game functions | 24,346 (1,614 functions) | 18,280 (1,183) | 28,690 (1,866; 4,690 through the overlay dispatch) |
| failures | 0 | 0 | 0 |
| save | 256 KiB flash | 8 KiB EEPROM | 256 KiB flash |
| stage played | "The Anthem" | "ループ&ループ" | "サムライブルー" |
| windowed play | timing and audio right (user) | timing and audio right (user) | works great (user), through the overlay swaps into a song |
Plus ndskit's instruction test: 57,781 vectors, 0 failures. OE2's overlays change hands as the game goes (6 then 1 at boot, 0 for the stage, 5 for gameplay, 6 again at the fail screen), each found by the runtime. Known runtime gap seen: 3D edge marking is not drawn (the EBA and OE2 stage maps).
The options bar (all three games)
EBA's window has a menu bar (src/eba_ui.cpp, Dear ImGui in
third_party/imgui, v1.92.9b):
- Game: screenshot (F12), quit (Esc).
- Display: resolution (Auto, or 1x to 6x), layout (vertical, side by
side, top screen only, bottom screen only; F9 cycles), frame rate (DS,
60, 120, 240, uncapped), Vsync (Auto: on when the display's refresh rate
suits the frame rate; On: always, no tearing; Off: never, the least
latency;
--vsync auto|on|off), fullscreen (F11, Alt+Enter), low latency. - Look: the filter. Skins and HD approach circles are switched off for
1.0 (
kSkinsinsrc/eba.h) until a full HD skin is made (see Future updates).
No menu has submenus (in a small window ImGui puts one over its menu): each choice is a list under a grey heading.
- Input: button mode on or off, rumble and its strength, up to four hit keys (click one, then press a key; right-click clears), the connected gamepads, the key map.
- Timing: the offset (-8 to +8 frames), the sound's latency (1 to 6 frames), and Calibrate..., a window that counts every tap the game judges by how many frames after its note it came (before the offset): a histogram, the average and spread, the grades, and Apply to set the offset to the average (from 20 taps).
Every change is made at once and saved in %APPDATA%\eba-recomp\EBA\settings.ini
(SDL's folder for the user's settings); ndsrun's own options win for a run
that gives them, and headless runs ignore the file. In a window the bar sits
above the screens and the window makes room for it; in fullscreen it shows
while the mouse is near the top; F1 hides or shows it. While the mouse is on
the bar or a menu, it is not the stylus, and Esc closes the menu, not the
game.
Timing offset (all three games)
The judge (0x020681D8, every tap and slider head; spinners are not timed)
is recompiled with a hook (--hook 020681D8); src/eba_timing.cpp gives it
the song's clock minus the offset, so a tap made N frames late is judged as
on time. The picture and the music are untouched, so it suits a player who
follows the sound or the picture alike. Checked headless with the button
mode's test player pressing every note 4 frames late: offset 0, every note
a 100 and the stage failed; offset 4, rank S, 258 of 258 at 300. Options:
--port offset=N, --port timing-log (every judged tap). The playtest that
asked for it: 241 presses averaged 1.8 frames late (about 30 ms), 99 of them
100s.
Display options (EBA's window; any ndskit game)
build/recomp/b/ndsrun roms/EBA.nds --save build/play/EBA.sav --window --scale 3 --fps 60 --layout side
All of these also change while the window runs (the options bar, or F9 and F11).
| option | what it does |
|---|---|
--layout stacked|side|top|bottom | the screens one above the other (default), side by side (for wide displays), or one alone. The stylus follows the lower screen wherever the layout puts it (none with the upper alone). |
--low-latency | waits for each picture to be drawn before the next (glFinish), so the driver queues none ahead of the display. |
--audio-latency F | the sound queued ahead of the device, in frames (3 by default; 1.5 held without a dropout at 60). |
--scale N | composes both screens at N times the DS's resolution (1-6): 1x 256x384, 2x 512x768, 3x 768x1152, 4x 1024x1536, 5x 1280x1920, 6x 1536x2304 (the first to cover a 4K display's height). The 3D draws at that resolution; the 2D layers stay the DS's pixels, sharp. The window opens at that size, fitted to the display; a larger window gets a sharp-bilinear upscale, a smaller one the mean of the pixels it covers (supersampling). Without --scale the picture follows the window's size, as before. |
--filter sharp|bilinear|bicubic|soft | the picture's filter, on the DS's pixels (Display > Filter): sharp as before; bilinear soft; bicubic (Catmull-Rom) smooth but fairly crisp; soft (a cubic B-spline) the most blended. The 3D is resampled with the 2D. |
--fullscreen | the whole display, letterboxed at the DS's shape. F11 or Alt+Enter toggles it. |
--fps ds | the default: the DS's 59.83 frames a second, presented with the vertical sync (on a 60 Hz display one frame shows twice every 5.8 s). |
--fps 60 / 120 / 240 | the game at exactly 60 frames a second (0.29% faster than the DS, the sound resampled with it: about 5 cents, inaudible), each frame shown 1, 2 or 4 times. On a display at that refresh rate the vertical sync is the clock, so every frame lasts the same: no judder. On another display, paced by the clock. |
--fps uncapped | the DS's pace, pictures as fast as the GPU draws them, without the vertical sync. |
The title bar shows the last second's frames and pictures. The log ends with
the run's averages (after its first 60 frames), how many frames came late,
the longest, and the longest the game itself took to run one; any stall
over 50 ms is noted as it happens. F12 also saves the composed picture at
full scale (shotN-3x.png).
Measured on this PC (RTX 4060, display 2715x1527 at 60.000 Hz, NVIDIA's "Vertical sync" left to the application), 20 s from the boot each:
--fps | game frames a second | pictures a second |
|---|---|---|
ds | 59.80 to 59.83 | 60, with the sync |
60 | 60.00, no frame late | 60.0, the sync the clock |
120 | 59.95 | 120.0 |
240 | 59.95 | 235.8 |
uncapped (fullscreen, 4x) | 59.73 | 2,166 |
The game itself never took more than 6 ms for a frame. On this 60 Hz display, 120 and 240 run without the sync, so they can tear; on a display at 120 or 240 Hz they lock to it the way 60 does here. Above 60, each extra picture repeats the game's last frame: the DS draws nothing between frames. The planned HD note layer will draw the hit circles, approach circles, slider balls and spinners between frames, which is what makes 120/240/uncapped smoother. If a driver keeps the vertical sync on against the runtime's request (NVIDIA Control Panel's "Vertical sync: On" did), the log says so.
Button mode (all three games)
For players without a touchscreen: any face button of an Xbox, PlayStation
or Switch controller, or Space on the keyboard, in place of the stylus during
a song. It is on in EBA's build (src/eba_buttons.cpp, linked into ndsrun
by cmake/eba.cmake).
- Hit circle: press on the beat. The stylus touches the note the game is judging, for two frames; the game judges it as its own tap, with its own windows (300 at +-2 frames, 100 at +-5, 50 at +-8, and a press more than 8 frames early is a miss, as with a stylus).
- Slider: press on its head and hold; the stylus follows the ball, a path point a frame, through every pass and the tail.
- Spinner: mash. Each press spins the stylus faster around the spinner
(2 turns a second per press, halving in 0.4 s without presses, at most 10
a second); about 6 presses a second keeps up with the hardest spinners.
Or turn the left stick round (past 60% of its throw): the stylus turns
with it, 1.5 times as far, either way, on top of any presses (at most 18
turns a second). Checked headless (
stick-auto=N, a stick turning N times a second): The Anthem with the stick at 5 a second scores 3,349,920, the same as mashing; held still, 3,322,660 (the spinner's bonus lost). Ouendan 2's first stage: 2,947,250 by stick, 2,949,450 by mashing. Or mash the triggers (L2, R2: a press past 60% of the pull, released under 35%), counted as presses on a spinner only. A DualSense's adaptive triggers push back while a spinner turns (Input > DualSense trigger resistance, on by default): continuous resistance (SDL's DualSense effect report, mode 1) of 120 (of 255) at the start, easing slowly with the spinner's speed (smoothed over about 2 seconds) to none at full speed; off after it. Headless (buttons-log): at 7.5 presses a second it eases 120 to 40 over 115 frames and is still easing when the finale spinner ends (it was 200 to 0 over 80 frames). - The mouse still works and wins while it is down. Outside a song the face buttons are the DS's (south B, east A, west Y, north X), for the menus.
Options, each as --port ...: buttons=off; button-keys=Space,F,J (SDL
key names); buttons-log (every press, the note it hit and how many frames
off); buttons-auto (a test player: presses on every note's frame, holds
the sliders, mashes the spinners; with stick-auto[=N] it spins them with a
left stick turning N times a second instead).
build/recomp/b/ndsrun roms/EBA.nds --save build/play/EBA.sav --window --scale 3 --fps 60 --port buttons-log
Checked headless: buttons-auto from the boot through The Anthem on Hard
Rock, everything through button mode: rank S, 258 of 258 at 300, max combo
408, the stage cleared. With no button pressed, runs are byte-identical to
before. What it reads of the game (the song clock, the notes' manager and
each note's fields) is at the top of src/eba_buttons.cpp;
tools/eba_notes.py inspects a RAM dump.
Rumble (Input menu, on by default, with a strength; --port rumble=off,
rumble-strength=N, rumble-log; src/eba_rumble.cpp), in button mode,
through SDL's rumble (a DualSense's haptics, a Switch Pro Controller's or
Joy-Con's HD rumble, Switch 2 controllers where SDL 3.4 drives them, Xbox
motors):
- high (both motors, 150 ms fading) for the note that ends a combo (the next note is number 1: a tap, a slider's tail) and every spinner's end;
- medium (60%, 100 ms) for every other note hit, a slider's head or tail, and a miss (its X);
- low (30%, 65 ms) for slider ticks, panned across the pad by the ball's x;
- nothing while a spinner turns (a DualSense's triggers push back instead).
Taps, slider heads and misses come from the judge's hook; ticks are the
combo going up without a judged tap; a tail is felt as the ball reaches its
end while held (Ouendan 1 leaves some tails out of its combo, before a
break). Checked headless with rumble-log on all three games' first
stages, every note accounted for. The Anthem: high 40 combo ends (13 of
them slider tails) and the finale spinner; medium 202 hits, 28 heads, 15
tails; low 122 ticks. Ouendan 2: high 35 and its spinner; medium 252, 16,
3; low 19. Ouendan 1: high 59; medium 168, 6; low 14 (its first stage has
no spinner). Pressing 10 frames late (buttons-auto=10): a medium pulse
for each of the 8 misses.
A face button (or a hit key) also taps SKIP at the lower right in a stage's
opening and ending comics (all three games), and in a song's intro after GO!
(the Agents' arrival, the episode's card, ...) up to its first note, where
the game has a SKIP there (EBA, Ouendan 2). Where it has none, the same
press fast-forwards instead (fast_forward, button mode or not): the
recomp's turbo runs the game at full speed, frames neither drawn nor paced,
the sound dropped, until 1.5 s before the first note appears (Ouendan 1's
intro) or until the song is over (every game's outro, after the last note:
EBA and Ouendan 1 never let the player skip it, Ouendan 2 does). Checked
headless: Ouendan 1 from song frame 479 to 767, the first note (948) then
played; its outro from 10006 to the results; EBA's outro from 9718 to the
ending comic and results. In a window the screen holds its last picture for
the second or two the fast-forward takes.
The d-pad hits like the face buttons (in songs and comics), so a stream can alternate between the two sets; keyboard hit keys have bits of their own.
The game's own play (src/eba_play.cpp, from the decomp)
The matching decompilation (eba-decomp, docs/DECOMP_FINDINGS.md) showed
what the judge (Note::Judge) reads of the session besides the touch:
-
Autoplay: session
+0x234(EBA) /+0x27C(Ouendan 2, the same load in its judge): 1, 2, 3 judge every note 300, 100, 50 on its own frame; 4 at random.--port autoplay=N. Checked: EBA's The Anthem, 257 at 300 and cleared (spinners too); Ouendan 2's Samurai Blue, 362 at 300 and cleared. -
Replays (saved replays are a future update; off in release builds): every judged tap goes into the session's replay
Array(+0xA0: data, capacity, count; 8-byte events: frame judged, the note's time, kind, result, a new group). A window's run saves each song's to<out>/replays/GAME_TAG_stage_time.repas it ends (--port replay-save=FILEheadless);--port replay=FILEloads one into the buffer the game makes for its own (as the song starts) and sets the replaying field (+0x138/+0x15C). Checked: a run pressing 4 frames late, recorded and played back with no input, gives the same 241 judgements. The game's replay mode does not drain the life gauge: a recorded fail plays back to a clear. -
Assists (all three, the Assist menu): the frames a note shows and the hit test's extra radius are .data the game sets for each stage (EBA 75 frames for The Anthem, 99 for Makes No Difference; Ouendan 2's first stage 72, Ouendan 1's 90; the radius +3 in all three): the approach is scaled from the game's value (+30%, +65%, -20%), the radius widened by 5 or 11 px. Off, they are left alone.
--port approach=PCT,--port hit-extra=PX. -
No-Fail (all three, the Assist menu): in a song, the life gauge's switch (an s32 the song's setup sets to 1; EBA's Training turns it off) is held at 0, so no judgement fills or drains it; off mid-song, it is set back to 1. Hide the life bar (on by default) leaves out its four textures meanwhile: the 気/Yes markers, the bar, and the red and yellow fills (the fills are the same textures in all three games). Customs keep their score with it. Checked headless with no input: The Anthem fails (Regroup?) by frame 8,000 without it, plays on at 13,000 with it; Ouendan 2 fails (応援失敗) by 8,000, plays to the stage's end with it; Ouendan 1 fails by 7,000, plays on at 11,500.
--port no-fail,--port hide-life=off.approach hit radius gauge switch its function (EBA's) EBA 0x020B47040x020B22E00x020B40C80x02067AD8Ouendan 2 0x020926440x0209003C0x020922000x020BAAD0Ouendan 1 0x020891B00x020861840x02088A7C0x0202F340Ouendan 2's were found by matching its code to EBA's: the function before each judge sets a note's appear frame from the approach (EBA
0x020681B8, Ouendan 20x020BB294); the judges load the radius at the same place; the gauge function loads the gauge table by judgement, then tests the switch.
Effects (all three games, optional, src/eba_fx.cpp)
Off by default (Effects menu; --port fx, --port fx-intensity=N). Nothing
shows before a 30x combo (the first combo burst); a broken combo fades it out
until the next 30x. The combo is the game's own counter (session +0xE0 EBA,
+0xF4 Ouendan 2, +0xBC Ouendan 1: each Combo's count, 0x2C before its
score; slider ticks counted, as shown). EBA's combo bursts (the Agent
portrait) come at 30, 60, 100 and 150, not 90.
- Fire (all three): fire rising behind the squad or the Agents, taller as the combo grows (full by 150x), flaring with each hit, a burst on an all-300 group (a toggle): the Ouendan games' own fire, heightened. EBA had spotlights for a while; they were removed (2026-10-25): new graphics made from scratch do not suit the games.
- Where: in the 3D frame, a pass after the polygons at a depth just in
front of the farthest opaque one (the stage's background), so the Agents,
the notes (both 3D) and the HUD are not lit; and under the 3D where a 2D
background shows through it (the compose pass).
--port fx-debugdraws it in front of everything;--port combo-loglogs the combo.
Local ndskit patches
In patches/, applied in this order to upstream ndskit (5ee24af); together
they reproduce the submodule's working tree exactly:
ndskit-discover.patch: two discovery fixes (below).ndskit-display-options.patch: the display options above (runtimedisplay.cpp,audio.cpp,main.cpp,nds.h,display.h). Headless runs are byte-identical with and without it.ndskit-input-hook.patch: one input path for the window and scripts (nds_apply_input) with a hook for a port's own code; SDL3 gamepads by the place of each button (south B, east A, west Y, north X, the d-pad, Start, Back, shoulders);PAD_*names in input scripts;--port KEY[=VALUE]options for the port.ndskit-display-live.patch: screen layouts (--layout, F9); every display setting changeable while the window runs (display_change); hooks for a port's UI (its first look at each event, drawing over the picture, a margin kept above the screens);--low-latencyand--audio-latency. The screens are now always composed into a picture at the scale and laid out from it.ndskit-function-hooks.patch:python -m ndskit.recomp --hook ADDRrecompiles a function asf_ADDR_origwithf_ADDRin its place, calling the hook a port sets witharm_set_hook(ArmModule::hooks). One of ndskit's own known gaps ("no hooks yet").ndskit-filters.patch:--filterand the out pass's smoothing filters;display_settings()reports a change not yet applied (the options bar saved the old value).- `ndskit-effects