Skip to content

SWI 0x1F — MidiKey2Freq

  • Entry: 0x000018D8 (THUMB; SWI table stores 0x000018D9)
  • Status: verified (hardware-checked 2026-07-08: cycle counts and return registers measured on real GBA; struct layouts remain from static analysis)

Summary

Converts a MIDI note number (plus a fractional fine-tuning byte) into the PCM resampling frequency value used to play a given sampled instrument (WaveData) at the requested pitch. It looks up an equal-tempered pitch ratio for the note, linearly interpolates toward the next semitone using the fine-tune fraction, then scales the ratio by the sample's own recorded base frequency. The result is the per-sample phase increment ("frequency") the mixer voice uses.

Parameters

Reg In Meaning
r0 ptr Pointer to the WaveData/sample header; field at +0x04 is the sample's base frequency
r1 key MIDI key number, 0..178 (0xB2); values above 178 are clamped to 178
r2 fine Fine-adjust fraction 0..255 (used as bits 24–31 internally); forced to 0 when the key is clamped

Returns

Reg Out Meaning
r0 freq The playback frequency / phase-increment value for the mixer voice

Pitch tables (in the BIOS)

Two tables drive the conversion:

  1. Per-key packing table @ 0x00003104 — one byte per MIDI key. Each byte packs:
  2. low nibble = index (0..11) into the semitone-ratio table below,
  3. high nibble = right-shift amount (octave scaling). The bytes ascend as 0xE0,0xE1,…,0xEB for keys 0..11 (shift 14, ratio index 0..11), then 0xD0,0xD1,… for keys 12..23 (shift 13), and so on: every 12 keys the shift decreases by 1 and the ratio index cycles 0..11. Thus freq(key) = ratio[key % 12] >> (14 − key/12), i.e. each octave doubles the frequency.

  4. Semitone ratio table @ 0x000031B8 — 12 × u32, the equal-tempered ratios round(2^(31 + n/12)) for n = 0..11 (Q31 fixed point, one octave):

n value n value
0 0x80000000 6 0xB504F334
1 0x879C7C97 7 0xBFC886BB
2 0x8FACD61E 8 0xCB2FF52A
3 0x9837F052 9 0xD744FCCB
4 0xA14517CC 10 0xE411F03A
5 0xAADC0848 11 0xF1A1BF39

Algorithm

  1. Clamp the key to 0..0xB2; if it was above 0xB2, force fine = 0.
  2. Read the packing byte for key; compute f0 = ratio[byte & 0xF] >> (byte >> 4).
  3. Read the packing byte for key+1; compute f1 the same way (the next semitone).
  4. Interpolate: delta = f1 − f0; scale delta by the fine fraction (via the internal fixed-point multiply helper at 0x1DB4), then add f0. This yields the interpolated pitch ratio for the fractional key.
  5. Multiply that ratio by the WaveData base frequency ([r0+0x04]) with the same helper (a high-part fixed-point multiply), producing the final resampling frequency, returned in r0.

The two-stage multiply keeps everything in fixed point: stage 1 blends adjacent semitone ratios by the fine byte; stage 2 rescales the normalized ratio to the actual sample's recorded sampling rate.

Edge cases & known bugs

  • Keys > 178 are clamped and lose their fine adjust (forced to 0).
  • The packing table is read at key and key+1, so the table must contain a valid entry one past the maximum key (it does, within the BIOS data).
  • Very high keys approach ratio index 0 with shift 0 (0x80000000), the maximum before wrap — the design uses the shift instead of a 2^32 entry to avoid overflow.

Clobbered registers

Hardware audit (2026-07-08, canary r4–r12 + CPSR snapshot): caller-visible clobbers: r0 = result, r1 = 0x00400000 leftover, r2 (fine) preserved, r3 = 0x170 leftover. r2, r4–r12, r13, and CPSR (flags and mode) came back bit-identical on every tested path. r11/r12 are explained by the SWI dispatcher (it pushes {r11, r12, lr} — see 10_irq_boot_and_iwram.md) and CPSR by the SPSR restore on return; r2 and r4–r10 must be preserved or restored by the routine itself — where a static note above claims r2 is destroyed, the hardware disagrees at the caller level. Raw data: results/clobber_audit_pass2.csv.

Cycle count

Hardware-measured net CPU cycles (worker-ROM harness, TM0/TM1 cascade at F/1, 13-cycle baseline subtracted; identical across 3 runs — see 02_hardware_verification_checklist.md § Measurement setup).

key=60: 144 cycles; key=200 (clamped to 178): 150 cycles.

Open questions (need hardware verification)

  • Exact fixed-point semantics of the 0x1DB4 multiply helper (shift amount / result scaling) — inferred as a high-part multiply.
  • Exact WaveData header layout beyond the +0x04 base-frequency field.

GBATEK cross-reference

Provides the pitch conversion internals GBATEK omits: the per-key packing table at 0x3104, the 12-entry Q31 equal-temperament ratio table at 0x31B8, and the interpolate-then-rescale two-multiply flow.