miminya.me ~ EZ2PORT compatibility

Projects › EZ2BMS › EZ2PORT compatibility

EZ2PORT compatibility

EZ2PORT (the owner's native EZ2AC engine) is the reference for everything EZ2BMS publishes. This page lists where EZ2BMS follows it exactly, where it deliberately does something else, and what is still unknown. Every "exact" row is proven by a test against the vendored engine core (crates/ez2port-oracle); the snapshot is EZ2PORT build 1582.

Exact (oracle-tested)

Area EZ2BMS Test
EZFF .ez read/write, v5-v8 io/ez/ezff.ts ezff.oracle.test.ts - every field, every record
Tempo map and every record's time (f32 BPM, ms) timing/engine-tempo.ts bit-identical milliseconds
.gds descriptors ez2data/gds.ts ez2data.oracle.test.ts
.pvi skins (tracks, target bar, note art) ez2data/pvi.ts same
.abm decode (all six header variants, 8/16/24/32-bit) ez2data/abm.ts RGBA hash identical
.abm encode (Final EX, 24-bit) ez2data/abm.ts byte-identical to ez2_abm_write
File cipher ez2data/crypt.ts byte-identical for random tables
Velocity/pan arithmetic ez2data/mixparam.ts same integers
Chart file names (mode, song, tier) modes/filenames.ts same as ez2_chart_id_parse
Mode lane sets modes/registry.ts same tracks as ez2/mode.c
Published package (song.ini, .ez, .ezi, .ini) publish/package.ts publish.oracle.test.ts - the engine reads the plan back
Lane notes and background sounds of a bmson publish/chart-plan.ts same records as the port's own ez2_bmson_import
Judgement, combo, gauge, score and the hold machine engine/score.ts engine.oracle.test.ts - random scripts, op for op
The synthetic player (tools/ez2judge.c) engine/judge-sim.ts same counts, score, gauge and grade
Published keysounds (.ssf, 16-bit 44.1 kHz stereo) ez2bms-audio cut.rs ez2port-oracle/tests/audio.rs - header and PCM hash
song.ini as the port reads and lists it publish/songini-read.ts songini.oracle.test.ts - ez2_usersongs_merge, random files
Disc (disc.abm) and stretched eyecatch (eyecatch.abm) ez2bms-media art.rs art.oracle.test.ts - byte-identical to write_disc / write_eyecatch, random sizes up and down
Title plates (songname.abm): layout, rasteriser, colour, halo, oblique, CJK faces ez2bms-media text.rs plate.oracle.test.ts - byte-identical to ez2_ttf_render_box / ez2_textspec_render, random text and plates
The song preview (preview.ssf): the importer's window, mix, fades, normalising ez2bms-audio preview.rs preview.oracle.test.ts - PCM identical to write_preview for random songs, mixed at unity
The BGA a bmson names ([Bga] File, StartMs): the earliest event's movie, its time through tempo changes and STOPs publish/bga.ts bga.oracle.test.ts - equal to ez2_bmson_import's for random charts (to 1 ms with BPMs f32 cannot hold, see below)
The song select's wheel: every disc and title plate placed, the scroll's chase, the disc's swing per tier ez2data/selectwheel.ts selectwheel.oracle.test.ts - placements within a thousandth of a pixel of ez2_select_wheel_place / _rail_place, chase and swing frame for frame
A stem chopped by M4's slicing (continuation notes only): its background sounds, slice for slice slice/ops.ts publish.oracle.test.ts - equal to ez2_bmson_import's records for random grids, resolutions and tempi; the engine reads each keysound as planned
bmson 0.21, and legacy beat-* lane numbering, as EZ2BMS opens them io/bmson/{v021,legacy-remap,open}.ts legacy-bmson.oracle.test.ts - publishes to ez2_bmson_import's records of the same file written as 1.0; a spec-numbered beat-10k to its records once renumbered EZ2's way
A chart's keysound index (.ezi) and settings (.ini) as an import reads them io/ez/ezi.ts, engine/songini.ts ezi-ini.test.ts - random files, every entry and value equal to ez2_ezi_parse / ez2_song_ini_parse, quirks included
A mode's song table (song.bin): its cipher, entries, levels, BPM, category groups and views, and which chart files and folder an entry has ez2data/songdb.ts songdb.test.ts - random tables (and cut ones) equal to ez2_songdb_parse / _category_view, bytes equal to ez2_songdb_decrypt for random tables, files and folders equal to ez2_songdb_charts / _song_dir; a real install's every table with EZ2_ROOT/EZ2_EXE
A game chart imported (.ez + .ezi + .ini): every note's time, its lane, and the chart republished io/ez/import.ts ez-import.test.ts - random charts (tempo records on any track, doubled and out of range; notes and holds on every track): every note's millisecond equal to ez2_tempo_ms, and the republished .ez giving back each lane's records (track, tick, keysound, velocity, hold) and the background sounds; every shipped song with EZ2_ROOT/EZ2_EXE
A game chart exported for the cabinet: every note's track, tick, time, sound, velocity, pan, kind and raw length; the kept records; the tempo map; the header publish/chart-plan.ts (target: 'cabinet') cabinet.oracle.test.ts - the synthetic game's charts (v8 and v6) and 60 random game charts, imported, compiled for the cabinet and read back by the port's parser, equal to the originals
A cabinet export's files: .ez/.ezi/.ini encrypted with the executable's tables, and song.bin patched in place publish/cabinet.ts, ez2data/songdb.ts cabinet-plan.test.ts - ez2_decrypt then the chart, .ezi and .ini readers give our plaintext; the patched table decrypts and parses with the new levels, and ez2_songdb_charts finds an added tier
Cabinet keysounds: 16-bit PCM rewrapped untouched, anything else cut as a publish cuts it ez2bms-audio export.rs ez2port-oracle tests/audio.rs - read by ez2_ssf_parse with the same samples
keys.ini and one binding token (a key, a pad button, a hat direction, an axis, the mouse, vtt): channels, alternates, quoting, comments, sections, the turntables, the first bad line, and the text written back input/{keyconf,bindspec}.ts input.oracle.test.ts - fixed and random files and tokens through ez2_keyconf_parse/format and ez2_bindspec_parse/format
settings.ini's Debounce (the only value EZ2BMS takes from it): read in 255-byte pieces as fgets reads it, ; comments, [/# lines, the key in any case, atoi, 0-100 only, the last one winning input/portcfg.ts input.oracle.test.ts - fixed and random files through ez2_portcfg_load (oracle portcfg)
Scroll-speed changes: the rate the field chases (speed × the chart's multiplier), the chase of a tenth of the gap per frame, a note's offset and y timing/scroll.ts scroll.oracle.test.ts - random scripts through ez2_scroll_target/_tick/_offset/_y, bit for bit
A package's scroll changes: every one (the chart's own, and those an older import kept) as a type-6 record on track 0 with the rate's f32 publish/chart-plan.ts scroll.oracle.test.ts - random charts with tempo changes, STOPs and kept records, read back by ez2_chart_parse at the ticks and words the chart says

Deliberate differences

What EZ2PORT's bmson importer EZ2BMS Why
Hold across a STOP converts the unshifted length: the hold gets shorter shifts the hold's end like any position keeps the hold as charted
Mode keywords in chart_name / file name first mode_hint as written; the keyword reading is linted an explicit field should not be overridden
Tier keywords (hd, shd, ex) in names info.x_tier, file named to match same
Two BPMs at one tick written; order then depends on the C library's qsort never written the engine's sort is not stable
Velocity, pan, hold kind fixed at 127 / 64 / 0 x_vel, x_pan, x_kind per note EZ2 charts use them
Judgement and gauge windows scaled from judge_rank, gauge fixed judgement_deltas / life_deltas written to the .ini per-chart, as the original game does
up (release) notes the whole note is dropped kept as a plain note; only the release re-trigger goes the press is still charted
Slice cut points rounded to 1 ms exact frames from the published f32 tempo consecutive slices join without a click
Background tracks first track free at that tick first track whose last sound has finished the original has one voice per track
End of the stage 26 frames after the last record a closing tempo record after the last sound's tail the last sound is not cut off
Disc crop the centred square the centred square unless you move or size it the jacket's subject is not always central
Eyecatch framing the whole image squeezed to 1024x512 2:1 art as the importer; other art a 4:3 crop filling the top-left 640x480 the select screen shows, carried on right and down a 4:3 or square jacket is not squashed
Transparent images alpha ignored: the colour under it shows composited onto black a transparent corner is the port's key colour, as intended
Photos turned by EXIF decoded as stored (the port's decoder hook) turned upright, as a browser shows them what you crop is what you see
Which chart names the art the first chart's info only the first chart (in mode and tier order) that names an image the same when one chart names it; never empty when another does
Title plate position baseline 23, a long title scaled down both ways the shipped plates' layout: baseline 22, condensed to 236 px, a subtitle on 27 lines up with the game's own titles on the wheel
Plate fonts the machine's bold sans; a Korean title may get boxes Roboto Bold and Noto Sans CJK Bold, shipped the same plate on every machine
A plate image of your own none (the importer always renders text) fit to 256x32 with the importer's box average, black kept as the see-through key art made for the wheel goes in as it is
Preview mix raw samples summed, velocity and pan ignored; only sounds of 20 s or more carried across the window's start the chart as the engine plays it: velocity, pan, voices cutting, every sound under way picked up the preview sounds like the song
Preview window 20 s from the first note a quarter of the way in, 1 s fades that by default; your own start (on a note, or anywhere), 5-30 s, your fades a song's best 20 s are rarely a quarter of the way in
A preview file of your own preview_music: its first 30 s, as they are, no fades your window of it, faded and normalised like a mix the wheel restarts it hard: unfaded, it clicks
BGA timing the event's time from the bmson's double BPMs the engine's clock (f32 BPMs), which may be 1 ms off the importer's at a rounding edge the movie keeps time with the notes the engine plays
BGA movie the first chart's earliest event only; copied under its own name the first chart naming one, or any movie you choose, started when you say; copied as bga.<ext> an ASCII name every build and file system reads
A BGA the port cannot play copied and written; the stage shows nothing refused by lint (AVI, MPEG, Theora, HEVC, AV1...), from the movie's headers found before publishing, not on the cabinet
bmson 0.21 refused: it reads 1.0 only upgraded to 1.0 as it opens (BmsONE's own mapping), saved as 1.0; the 0.21 total kept as x_total_v021 old BmsONE files open
beat-10k numbered the bmson spec's way (2P keys x 9-13, scratch 16) always EZ2's numbering (2P keys x 11-15): the 2P side lands two lanes off notes on x 9 or 10 mean the spec's numbering; 2P notes only on x 11-13 fit both and are read the port's way, with a warning in Issues BmsTWO's BMS import and other spec tools write it
.ezi lines named like MIDI keys (C#0 1 mix-st.wav) atol'd to note 0, as the original does: every such line lands on one slot read as octave x 12 + semitone, so each note of the chart finds its own sound old charts use them; checked against a real install (ezi-ini.test.ts, EZ2_ROOT/EZ2_EXE)
An imported original's category (a package's is its song.ini's) the first version bank (1st ... TT) its song.bin lists it in, else CUSTOM HOT/NEW/ALL, level and alphabet banks say nothing about the song

Followed from the port's platform code (not in the oracle)

The mixer lives in EZ2PORT's platform layer (platform/common/ezaudio.c), outside the vendored core, so these are transcribed and unit-tested rather than compared against C: a retrigger restarts the sound on its voice; backing and autoplay play on the sample's own voice, presses on the lane's; level is 10^(dB/2000) with -100 dB as silence; pan turns one side down and leaves the other; the output passes a soft knee at 0.9 and a clamp.

Two deliberate differences: EZ2BMS resamples with a proper filter (the port steps through a non-44.1 kHz sample nearest-neighbour), and it publishes every keysound at 44.1 kHz so the port never has to resample one.

The game's own play field (scene/skin.c, not in the oracle)

With a game folder set, the playfield draws with the mode's own panel, the way EZ2PORT's scene/skin.c does. That file is outside the vendored core, so this is transcribed and checked with unit and Playwright tests on a synthetic panel (apps/editor/src/bridge/demo-skin.ts), not compared against C.

  • Which files. system/<Mode>/panel/STYLE_<Mode>1_<player>.pvi - style 1, the plain panel. Every path component matches in any case; a texture reference is tried as .abm first (the name up to its first dot), then as written, in the panel folder and then under the game root; a numbered series stops at the first gap; exact black is transparent, as on the cabinet. A texture the folder lacks is not drawn and is listed in the EZ2PORT tab.
  • Which track is which lane. Lane i of the mode's .gds (player one's slot, in file order) draws with [Tracki+1]. Without a .gds the bundled order is used, in which 5KeyMix's and ScratchMix's keys are Track2-Track6 (their descriptors open on the turntable).
  • Drawn as the port draws them: one black gradient (alpha 0x96 to 0xff) under every Track box, only when Track1's colours are set - BkColor1/2 are not lane fills; the border lines, outside the lane; each note at its texture's own size, centred in the lane, in the colour variant picked by the position in the beat (variant 0 for a third of the beat, then 1-4 a sixth each); holds as the three-slice bar (top half, one stretched middle row, bottom half); the beam lines above and below each note (a fourteenth of the lane's height at the scroll rate, BkColor1's alpha, each channel scaled 255/200); the key panel at its texture's size; the target bar, its frame cycling with the beat, additive, swaying a pixel or two while playing; the measure-line texture; the press glow and press beam while a key is down.
  • Different on purpose. The field is centred in the window rather than at the panel's x, with the judge line as far above the bottom as on the 480-line screen. The press beam is drawn at full height while the key is down (the port grows and shrinks it). The gauge, score, combo, judgement clips, bombs and groove lights are not drawn; the editor's HUD shows those. D3D blend pairs are reduced to normal or additive. Black, CV2Mix and the other styles and note skins are not offered yet.
  • 2P. The game's 2P panel keeps 1P's lane order, moved to the right (the port measured the original doing so), and the editor follows the panel. The neon skin still mirrors 2P, as BmsTWO's P2 skins do.

Classic mode: keying never changes the music (not in the oracle)

Classic mode (BmsTWO's Classic BMS Mode) charts over a song that is already complete in the background: placing a note keys a note the background already has at that spot, deleting sends it back. Its promise is that what autoplay plays does not change, and it is kept by checking, not by construction: every Classic edit - key, un-key, a lane move, split, heal, reset all - is dry-run first and refused, with the reason, if anything audible would differ.

Placing never adds a note (the owner's rule, 2026-09-28). A lane note plays only when it is hit: a slice keyed where the stem had no cut would be a new sound on the lane, heard alone on an early or late press and missing from the music on a miss. So placing only moves a background note that is already there onto the lane (keyed notes on other lanes at the same spot come after it, as in BmsTWO's FindSampleChannelAtTime), and where there is none it is refused. BmsTWO instead splits the sample still sounding there (FindSoundingSampleChannelAtTime); EZ2BMS keeps that split for the explicit right-click and the stem strips. A recorded take follows the same rule.

The magnet brings a placed note onto such a note. BmsTWO's SnapToSampleInCurrentGroup pulls to the nearest note of the current channel's name group, keyed or not, however far away. EZ2BMS pulls to background notes only, within a measure: first those of the picked sound's group (the backing track of the note last keyed or clicked; clicking a note picks its sound, as BmsTWO's click does), then any; beyond a measure there is nothing to key (Alt places freely, and is then refused). A spot the lane cannot take (a note there, or a hold over it) is passed over. A recorded press is pulled the same way, within half a grid step.

What "audible" means is chart-core's publish/audible.ts, built from the same per-channel code as the publisher (ChartClock, channelEvents). For each sound file, every event becomes a segment: when it starts, which part of the sample (the published frame cut, 22-frame minimum included), until when, at what velocity and pan. An event plays on the voice of its keysound identity (file, first frame, last frame), and the next event on that voice cuts it - the editor's engine and EZ2PORT agree here because the port's voice is the keysound's .ezi slot, and the publisher gives each identity exactly one slot. Contiguous pieces are merged (only when both the time and the sample frames meet), so a slice chain and the whole sound it was cut from compare equal. A sample of unknown length keeps a symbolic end, valid whatever the length turns out to be.

Tested by audible.test.ts (one test per rule: cuts, merges, a sound ringing past a chain, STOPs, same-tick ties), a model-based run over random songs and random Classic edits with undo and redo (classic.test.ts: the whole chart's fingerprint never moves), the real mixer (audible.render.test.ts renders both sides of 120 random cases at 44.1 and 48 kHz: equal fingerprints render within 1e-5 - float summation order - and the check has teeth: most different ones differ audibly), and classic.spec.ts in the editor.

What the check cannot see, and so is not promised:

  • Frame timing in the port. EZ2PORT fires sounds per video frame, the background before the lanes within a frame; the editor orders a tick's events by lane. Moving a sound between the background and a lane changes that order only for two events on one voice in one frame, and such ties are refused - but two different voices starting in one frame may start a frame apart in the port either way.
  • Two players. With both sides seated the port pans lane sounds to the player's side; the promise is for one player (and for autoplay).
  • Misses. In test play a missed note is silent, as in the game, so a keyed sound is only heard when it is hit. That is what keying means.
  • The original cabinet. The original executable has one voice per track, not per keysound: on a lane a keyed sound is cut by the lane's next note. The promise is for EZ2PORT; a cabinet export lints it (cabinet-voice-lane, exporting.md).
  • Unknown lengths. A sound that has not loaded is never taken to be sounding, so Classic offers less (and refuses more) until it loads.
  • Not ported from BmsTWO: the automatic split at a long note's release, and "right-click clears x_stop" (xStop is not published).

Slicing stems (M4)

Slicing writes nothing EZ2PORT does not already read: a cut is a background continuation note (c: true), a keyed slice a lane note, and the package is the one M1 publishes (slicing.md). So a chopped stem is in the oracle above - our package's background sounds are the importer's own, slice for slice - and cuts join sample-exactly into the uncut stem (the importer's 1 ms rounding is the deliberate difference already listed).

Like Classic mode, slicing promises that autoplay sounds the same after every cut, move, heal or key, and keeps it by checking: each edit is dry-run through publish/audible.ts and refused if anything audible would differ (slice.test.ts: a model-based run of random slicing never moves the fingerprint, and chopped stems render through the real mixer within 1e-5 of the uncut stem at 44.1 and 48 kHz). The same caveats as Classic mode apply (frame timing in the port, two players, the original cabinet's one voice per track).

What is EZ2BMS's own, with no counterpart in the port:

  • Onsets and tempo (ez2bms-audio analysis.rs) are suggestions shown on the strip; nothing is cut until asked. They are tested on signals made in the tests, not on the port.
  • The disk cache keeps long files decoded exactly as the decoder gives them (a hit is compared bit for bit with a decode), so a package is the same whether the cache had a stem or not. The crate's version is in every entry's name: a new build never reads an older build's audio.
  • Silence when chopping is judged on the engine's peak mipmap at -48 dBFS; a quieter stretch stays on the end of the slice before.

The BGA movie (scene/bga.c, media/video.c, not in the oracle)

A package's [Bga] movie is played by the port's scene/bga.c through ffmpeg, outside the vendored core; EZ2BMS follows that code and checks what it can from the movie's headers (chart-core media/movie.ts, tested on containers built by hand).

  • What plays. The Windows build's ffmpeg is cut down to the readers asf, mov (MP4, MOV), matroska (MKV, WebM) and ogg, and the video decoders for WMV 7-9 / VC-1, the Microsoft MPEG-4 variants, H.264, MPEG-4 part 2, VP8, VP9 and (for images) Motion JPEG (thirdparty/fetch-ffmpeg.sh). Anything else is a lint error. The Linux build links the distribution's full ffmpeg and plays more.
  • How it shows. Frame 0 at StartMs of chart time; nothing before it; every frame converted to RGBA at its own size and stretched to 640x480 behind the play field; nothing after the last frame - it never loops; its sound is not played. The BGA page's preview does the same with the webview's own player, which is not the port's decoder: a movie the webview cannot show may still play in EZ2PORT, and the page says so.
  • Size. A 1280x960 H.264 movie "drew nothing on the cabinet" (the port's own note in fetch-ffmpeg.sh): lint warns above 1024 on either side.
  • Test in EZ2PORT passes --bga, so the movie shows whatever the operator ini says.

The song select as the wheel preview draws it (select.c, not in the oracle)

The Song manager's "Wheel" page draws the song on EZ2PORT's song select. Where each disc and plate sits, the chase and the swing are the oracle-checked selectwheel.ts; the drawing around them follows tools/ez2play/select.c, outside the vendored core, and is checked with Playwright on the synthetic game folder (bridge/demo-skin.ts).

  • The carousel. A disc on the arc is system\disc\disc-mask.bmp multiplied (blend 9,6), its thumb added (2,2), shape_mask.bmp multiplied 3 px larger - all tinted by the placement's brightness. The disc at rest under the cursor, with a disc to show, is the focus: the mask twice and the art turned by the swing, the ring still. Blend (9,6) with a tint b is exactly canvas multiply at alpha b, and (2,2) is lighter.
  • The rail. System\SongSelect\VF\b_mask_2.bmp multiplied at (0, 64), its own width by 490; every plate added at its own size, its left edge white and its right edge the row's band (a negative band makes the original's ffffffXX), half a pixel in each way.
  • The dwell. The preview starts when the wheel has not moved for more than 30 ticks, and on the screen's first tick; a step stops it at once.
  • The exit eyecatch. The eyecatch at its own size from (0, 0), replacing; system\Channel_Eyecatch\common\Stage_Mask.bmp multiplied, Stage_1.bmp added, faded in from black ten levels a tick.
  • Different on purpose. The animated backdrop, the rail cursor and the frame (.str, Milestone 8) are a neon stand-in; so is any mask the game folder lacks. The neighbours are made-up songs, their plates rendered like this song's. Canvas filters with its own bilinear sampling, and ticks at 60 Hz from the display's frames.
  • What it shows about the port. A package's disc has no picture while it flies along the arc: the port looks for that thumbnail in the game tree's system\discsmall, never in the package (a request in ez2port-requests.md). A song without a disc is never the turning focus; its masks alone show.

Importing the game's own songs (M5, not in the oracle)

  • What is kept but not published: scroll-speed records (type 6, which the engine plays: 6333 of them in 24 shipped charts), track volumes, beats-per-measure, marks and the engine-ignored stops, in x_ez_records; Issues says so for each chart. A background note's length is kept as x_len: publish writes background notes as taps, as the engine plays them.

  • Voices: each .ezi slot is a voice in EZ2PORT; after an import two slots naming one file are one channel, so one voice. Issues counts them.

  • No v4/v5 BPM correction: some tools (rizu, the Bible) scale old charts' tempo by 0.99723; EZ2PORT does not, and neither does the import.

  • A disc per difficulty. The game's system\disc\<key>-hd (-shd, -ex) come across as the song's tier discs (discs in the song file); the wheel preview shows the tier's once the swing passes 180 degrees and the NM face before, as select.c draws bigdisc/bigdisc_nm. A package can carry one disc only; EZ2BMS writes the others as Disc.HD and friends, which build 1582 ignores (request 12 in ez2port-requests.md).

  • The disc and eyecatch are looked for where the port's song select looks (tools/ez2play/select.c): system\disc\<key>.abm, then a tier's <key>-hd/-shd/-ex, and system\eyecatch\<key>.bmp (which the vfs resolves to .abm), by the table's key in any case. They are decoded as ez2/abm.c decodes (the oracle-checked decodeAbm) and written as plain 24-bit BMPs; the colour key is the game's drawing, so black stays black.

  • Titles come from the port's text/manifest.songs.ini (beside ez2play, else <game>/text), read at run time: the game itself has titles only as plate bitmaps. The reading follows textspec.c read_manifest/parse_line (a ; outside quotes is a comment, the outermost quotes of a line's first field are the words, a second quoted line is the subtitle); there is no oracle command for it because the port renders the lines rather than returning them. Without the manifest the title is the song's key.

Importing BMS (M5, not in the oracle)

EZ2PORT reads no BMS, so there is nothing of the port's to compare with. The reading follows the BMS command memo as LR2, beatoraja (jbms-parser) and BmsTWO read it, and bms.test.ts checks it against the memo's own arithmetic: for random files (measure lengths, tempo changes on channels 03 and 08, STOPs, any slot grid) every note's position is exact and its time within a nanosecond of the memo's. Where players disagree, EZ2BMS follows beatoraja: a later line wins a tempo change at one spot, #BASE 62 is read before anything else, EUC-KR and Shift-JIS are both in the wild. What an EZ2 chart cannot hold - hidden notes, mines, scroll and speed changes, image BGAs - is left out or kept unplayed, and Issues says which.

Korean text is CP949 (windows-949) as the game's Windows and every browser read it. Node's TextDecoder('euc-kr') (ICU) is plain EUC-KR - KS X 1001 only, without CP949's 8822 extra Hangul syllables or the euro and registered signs of KS X 1001:1998 - so the unit tests, which run in Node, decode such a file without those characters, while the editor, which decodes in its webview, reads them. Writing is the same everywhere: io/legacy-text.ts takes only the KS X 1001 region from the decoder (Node and Chromium agree on all 8224 pairs) and computes the rest, which is Unified Hangul Code's own definition (checked against Chromium's decoder: all 8822).

Cutting a stem at a MIDI file's notes (M5, not in the oracle)

EZ2PORT reads no MIDI. The file says only where to cut: the cuts are M4's (standard continuation notes, each checked to keep the sound), so what the package holds is covered by the chopped-stem row above. Taking the MIDI's tempo is an ordinary tempo edit, published like any other; the plan counts the other notes it moves in time before you agree to it.

Exporting to the original game (M6, the executable not in the oracle)

The oracle is EZ2PORT's core, not the original executable, so what the export writes is proven readable by the port's own readers (the rows above), and where the original may differ is linted from what is written down of it:

  • 131068 bytes per file. The original decrypts a chart, .ezi or .ini into one stack frame of header[128] data[131072] out[131068] (third_party/ez2port-core/ez2/crypt.c); a bigger file cannot load (cabinet-size).
  • One sound per track. Its sound entries are indexed by the chart's track (reference/play.c), so a new sound on a track cuts the last, keyed or not (cabinet-voice-backing, cabinet-voice-lane).
  • 2047 keysounds (ez2/ezi.h: a 0x800-entry table, slot 0 unused), every slice counted (cabinet-slots).

Where an export deliberately differs from the game's own files:

What The game's chart Exported
Tempo records on any track, duplicates at one tick on track 0, one per change: the tempo map, which is equal, is what is promised
.ezi slots the author's numbering; two slots may name a file numbered afresh; two slots naming one file are one (the import made them one voice)
New background sounds - placed around the game's own, on free tracks, never over one; tracks added rather than a sound cut
Keysound files - never written over: equal audio reuses the file, anything else is name~n
.ini as shipped written only when its values change, as Key=value with CRLF
The end of the stage the last record an imported chart keeps its total_ticks and gets no closing record; a new chart gets publish's
song.bin a a per-tier float whose meaning is not known left alone; a new tier takes its siblings' when they agree, else 0
A missing sound a dangling name that plays nothing the same: its .ezi line kept, no file written

What the owner's cabinet must confirm is listed in AI-DISCLOSURE.md.

Exporting BMS (M6, not read by EZ2PORT)

EZ2PORT reads no BMS. The writer is checked by EZ2BMS's own BMS reader, which bms.test.ts checks against the BMS command memo's arithmetic: random charts written, read and converted back give every note in its lane, at its exact beat, with its length and sound, and at its time to the microsecond (bms-write.test.ts). What BMS cannot hold (velocity, pan, an EZ2 chart's kept records) is said in the dialog.

Controllers (the port's input layer, not in the oracle)

EZ2PORT's input layer is platform code on SDL (platform/common/ezinput.c, ezpad.c, vendored unbuilt in third_party/ez2port-core/reference), so the oracle cannot run it. input/mapper.ts transcribes it, and input-mapper.test.ts holds the port's own cases for it (tests/test_inputchannel.c) plus the turntable's rules:

  • a channel is the OR of its bindings; a key repeat is not a press;
  • an 8 ms debounce per channel, the level read again at the next event;
  • hat directions exact (a diagonal is its own direction);
  • an axis turntable in 256 units a swing, raising the scratch channels on any movement and holding them 90 ms, a reversal dropping the other; a velocity axis integrated once per 1/60 s.

ScratchMix's fret-and-strum is the port's play loop (reference/play.c 2426-2501, also unbuilt) in engine/strum.ts, tested rule by rule in strum.test.ts: a key alone is silent and unjudged, the turntable plays and judges the keys held on its side (not the pedal), and a key pressed within six frames after a strum still counts. The port measures that latch on its frame clock; EZ2BMS on the events' own times, which is what they are judged at.

Pads are read with the same SDL the port uses (crates/ez2bms-input): a board is vid:pid, the n-th more of a make vid:pid#n in SDL's order (ezpad.c key_for/count_same), and a hot-plug renumbers them. A press is placed by its age (press = now - age), and an age over 200 ms is taken as now, as the port does; input/hub.svelte.ts applies both to keys and pads alike. settings.ini's Debounce is read as ez2/portcfg.c reads it (input/portcfg.ts, against the oracle's portcfg in input.oracle.test.ts); keys.ini is looked for where the port keeps it (ez2/cfgdir.c, ez2bms-launch config.rs). EZ2BMS reads both files and never writes them: its bindings are its own, taken from the port's file the first time.

Where EZ2BMS departs, deliberately:

  • times are float ms on the audio engine's host clock, where the port uses SDL's integer ms;
  • a turntable hold ends exactly at its deadline, not at the next frame;
  • an edge timed before a channel's last is not chatter (the port's unsigned subtraction takes it as such);
  • the seat (2P playing on 1P's side) and the mouse and virtual turntables' positions are left out: nothing in the editor reads them.
  • the bindings are EZ2BMS's own: the port's keys.ini is read (the first time, and by Import) and never written;
  • the audio, picture and input offsets, and the latency tests that set them, are EZ2BMS's only: EZ2PORT has no offsets, so a chart plays in the port exactly as it did;
  • a debounced edge with nothing after it waits for the next event, as in the port (a request in ez2port-requests.md §11), where re-reading once the window ends would free it.

Scroll-speed changes (M8; the play loop not in the oracle)

The field's arithmetic is ez2/scroll.c, oracle-checked above. What feeds it is the play loop (reference/play.c), which the oracle does not build, so these follow its code:

  • The multiplier in force is the last change at or before the cursor's fractional tick, 1.0 before the first; the port resets it every stage (scroll_mult_advance). EZ2BMS: multiplierAt.
  • Two changes on one tick: the port sorts with qsort, which does not say which of the two comes last. EZ2BMS takes the later one in the chart's list, and lint (scroll-same-tick) warns when they differ.
  • At most 4096 are kept, the first collected (tracks in order, records in order). A package has them all on track 0 by tick, so the earliest stay; lint (scroll-count) warns past 4096.
  • They do not keep the stage open. The port takes type-6 records out before it finds the stage's end, where the original queues them with everything else. A package's closing record is written after the last sound whatever scroll changes come later; a cabinet export counts them, as the original does.
  • Where they go: a package puts every one on track 0 with a second word of 0 (the port reads only the first). A cabinet export puts each on the track, and with the second word, it was imported with.
  • The editor's Play view scrolls at the speed times the multiplier at the cursor, and while the chart plays it closes a tenth of the gap per 1/60 s elapsed rather than per frame, so it eases as fast at 144 Hz as the port does at 60. F5 passes the Play view's speed (--speed), so a test in EZ2PORT scrolls as the editor shows it.
  • Volume records (type 2) from a game chart are kept and written back to the cabinet, which mixes the track at them; EZ2PORT plays every track at full level and ignores them. EZ2BMS shows them and authors none.
  • The port's bmson importer does not read x_scroll_events: a raw bmson dropped in the songs folder plays without them. EZ2BMS writes packages, so its own charts are not affected (a request in ez2port-requests.md).

Port behaviour worth knowing

  • A package is listed in one bank. ez2_usersongs_merge adds it to its Category's bank only, where every shipped table also fills ALL: a user song is never in ALL (a request in ez2port-requests.md).

  • Some hold kinds make 100% unreachable, or pass it. The maximum score is notes * 300, with notes from the engine's counter (ez2_note_counted). Kinds 4 and 5 are counted by the counter's own ladder but pay a single instalment, so a perfect play falls short. Kinds 9-12 are not counted, yet their head still scores. Play mode reproduces both (a synthetic 7K chart scores 121 of 156 under autoplay, as the port's own tool does); lint flags these kinds on holds, and the Inspector says what the selected holds are judged and counted as.

Plates: how the renderer matches

The plate renderer is the port's ez2_ttf_render_box and ez2_textspec_render transcribed to Rust around the same stb_truetype (v1.26 as the port vendors it; upstream has since changed composite glyphs, see third_party/stb/PROVENANCE.md), with the f32 arithmetic in the same order. Both are built with NDEBUG, as EZ2PORT's builds are: stb asserts on some tiny glyph edges (a hyphen four pixels tall), and a build without it aborts where the port renders on. Equality is proven on Linux x86-64; the stb C is compiled without floating-point contraction so other machines round the same way, which the owner's Windows machine can confirm.

Open questions

  • What the eyecatch shows. It is drawn at its own size into the 640x480 select screen (tools/ez2play/select.c), so only its top-left 640x480 should be visible, and the visible framing is built on that. How a widescreen build shows it has not been seen on a real screen.

  • 5 KEY ONLY and SCRATCH lanes. docs/gds-slots.md lists the turntable and pedal tracks (10, 11) in the shipped 5keymix and ScratchMix descriptors, while ez2/mode.c and every official chart use keys 1-5 only. EZ2BMS charts those modes on keys 1-5; with a game folder configured it reads the real .gds, and lint reports turntable/pedal notes in those modes.

  • Andromeda and Catch have no bmson convention and EZ2PORT's packages cannot carry them yet; EZ2BMS keeps them for cabinet export only.

  • Legacy .ezi note names. Reading C#0 as octave x 12 + semitone comes from the charts' own keysound numbers, not from the game's code (which, like EZ2PORT, reads it as 0). The real-install test (ezi-ini.test.ts) checks that it never finds fewer of a chart's keysounds than the port's reading; it has not been run on a game yet.

  • An imported original's category. Its first version bank in the song tables is a choice, not a rule of the game (a song sits in several banks); CUSTOM when it has none.

This page is synced from docs/ez2port-compat.md in the EZ2BMS repo.

Updated 18 Oct 2026

9 projects

Display Properties