Distortion — HLE reconstruction and DSP bytecode

DSPHLE selector 0x0f · program image prog32_distortion · Preview (topology decoded and reconstructed; most panel→cell role mappings are position-decoded).

AGC waveshaper: DRIVE=0x00, VOLUME=0x02 measured; tanh clip is a labelled stand-in (class-6 table not undumped, open decode).

This page pairs the bytecode the effect runs on the NEC µPD6383GF with the high-level reconstruction that makes it audible in MAME. The reconstruction is not the chip’s microcode — it is a textbook DSP block built from the decoded meaning of that microcode, and reading the two together is how the bytecode is understood (and, in time, driven toward a faithful low-level emulation). See also the signal-flow flowchart.

The bytecode below is the source of truth; the HLE reconstruction is not. The HLE is our best current interpretation and may contain mistakes — where the two disagree, the bytecode wins, and a better HLE should be updated here. This page is the permanent archive of the reference HLE: it is kept here even after the code is eventually removed from the MAME sources.

DSP bytecode (reverse-engineered microprogram)

The disassembled image the chip executes for this effect. Source (regenerable): dsp/disasm/prog32_distortion.dsm.

; Reverse-engineered disassembly of the KN5000 effects-DSP microcode
; (NEC uPD6383GF), recovered from the original firmware.  The microcode is the
; work of its original authors; this is a disassembly for preservation and
; interoperability, and no claim of copyright is made over the disassembled program.
; KN5000 effects-DSP program -- DISTORTION
; image rep algo 32  |  slots 32  |  unit 0 (I-RAM load 84)
; family distortion  |  confidence high  |  42 words, 6 class-A multiplies (4 named)
; role: distortion: AGC waveshaper, curve A
; coefficient cursor base 0x00
;
; GENERATED by dsp/tools/gen_dsp_disasm.py -- DO NOT EDIT.
; Put labels/comments in the matching dsp/sym/*.sym; analysis in dsp/algorithms/.

  w0    00400008BC   ?word   0x00400008BC   ; 040.0.00.8BC  hi12{?6 res=040}  [SPECULATIVE: lo12 bit-11 modifier word + pointer-mode (bit11-family); the base selector/register is OPEN]
  w1    002E200000   ?word   0x002E200000   ; 02E.2.00.000  hi12{f31=7 ?5 res=020}  [SPECULATIVE (prospective, not measured): SRC 0x00 = mem[ptr]/delay-RAM read]
  w2    0000200415   ld      acc,(p)+0
  w3    0212200000   mac.b   (p)0,(p)+0 ; mem[p]<-acc, acc=0
  w4    0000A00415   ld      acc,c+,(p)+0
        ; C-RAM[0x00] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x00] = op0x61[0] (role coeff, INFERRED)
  w5    0212200000   mac.b   (p)0,(p)+0 ; mem[p]<-acc, acc=0
  w6    00922F9700   ?word   0x00922F9700   ; 092.2.F9.700  hi12{ST f31=1 ?7 res=080}  [SPECULATIVE (prospective, not measured): SRC 0x1C = control/mod source into MAC (100% MAC-consumed; LFO in mod fx, envelope/AGC in dynamics) -- NOT LFO-only: present in 19 non-LFO programs (dsp_datapath_fingerprint)]
  w7    0000A00415   ld      acc,c+,(p)+0
        ; C-RAM[0x01] (coeff, base 0x00 MEASURED)
  w8    0182207000   mac.b   (p)0,(p)+7
  w9    0040000C63   ?word   0x0040000C63   ; 040.0.00.C63  hi12{?6 res=040}  [SPECULATIVE (prospective, not measured): SRC 0x11 = ACCB (2nd accumulator)]
  w10   00006284CD   ?word   0x00006284CD   ; 000.6.28.4CD  hi12{-}  [table-lookup idiom, class-6 addr8 = table selector (INFERRED)]
  w11   00124011CE   ?word   0x00124011CE   ; 012.4.01.1CE  hi12{ST f31=1}  [table-lookup idiom, third word (INFERRED)]
  w12   01042001CE   post    (p),(p)+0
  w13   0102200000   mac.b   (p)0,(p)+0
  w14   0000A00415   ld      acc,c+,(p)+0
        ; C-RAM[0x02] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x02] = op0x62[0] (role output-level, INFERRED)
  w15   02122091CD   mac     (p),(p)+9 ; mem[p]<-acc, acc=0
  w16   00002F040E   ld      acc,(p)-16
  w17   0212210000   mac.b   (p)0,(p)+16 ; mem[p]<-acc, acc=0
  w18   0028200000   ?word   0x0028200000   ; 028.2.00.000  hi12{f31=4 ?5 res=020}  [SPECULATIVE (prospective, not measured): SRC 0x00 = mem[ptr]/delay-RAM read]
  w19   0880130407   dly.r  dsc[k],p+48
  w20   0000201000   nop
  w21   00122F61C0   mac.b   (p),(p)-10 ; mem[p]<-acc, acc=0
  w22   002E200000   ?word   0x002E200000   ; 02E.2.00.000  hi12{f31=7 ?5 res=020}  [SPECULATIVE (prospective, not measured): SRC 0x00 = mem[ptr]/delay-RAM read]
  w23   0000200415   ld      acc,(p)+0
  w24   0212200000   mac.b   (p)0,(p)+0 ; mem[p]<-acc, acc=0
  w25   0000A00415   ld      acc,c+,(p)+0
        ; C-RAM[0x03] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x03] = op0x61[1] (role coeff, INFERRED)
  w26   0212200000   mac.b   (p)0,(p)+0 ; mem[p]<-acc, acc=0
  w27   009220A700   ?word   0x009220A700   ; 092.2.0A.700  hi12{ST f31=1 ?7 res=080}  [SPECULATIVE (prospective, not measured): SRC 0x1C = control/mod source into MAC (100% MAC-consumed; LFO in mod fx, envelope/AGC in dynamics) -- NOT LFO-only: present in 19 non-LFO programs (dsp_datapath_fingerprint)]
  w28   0000A00415   ld      acc,c+,(p)+0
        ; C-RAM[0x04] (coeff, base 0x00 MEASURED)
  w29   01822F6000   mac.b   (p)0,(p)-10
  w30   0040000C63   ?word   0x0040000C63   ; 040.0.00.C63  hi12{?6 res=040}  [SPECULATIVE (prospective, not measured): SRC 0x11 = ACCB (2nd accumulator)]
  w31   00006284CD   ?word   0x00006284CD   ; 000.6.28.4CD  hi12{-}  [table-lookup idiom, class-6 addr8 = table selector (INFERRED)]
  w32   00124011CE   ?word   0x00124011CE   ; 012.4.01.1CE  hi12{ST f31=1}  [table-lookup idiom, third word (INFERRED)]
  w33   01042001CE   post    (p),(p)+0
  w34   0102200000   mac.b   (p)0,(p)+0
  w35   0000A00415   ld      acc,c+,(p)+0
        ; C-RAM[0x05] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x05] = op0x62[1] (role output-level, INFERRED)
  w36   02122FB1CD   mac     (p),(p)-5 ; mem[p]<-acc, acc=0
  w37   000020F40E   ld      acc,(p)+15
  w38   02122F1000   mac.b   (p)0,(p)-15 ; mem[p]<-acc, acc=0
  w39   0028200000   ?word   0x0028200000   ; 028.2.00.000  hi12{f31=4 ?5 res=020}  [SPECULATIVE (prospective, not measured): SRC 0x00 = mem[ptr]/delay-RAM read]
  w40   0880130000   dly.r  dsc[k],p+48
  w41   040010E000   endblk  #0E             ; END OF BLOCK -- the last word of a block

HLE reconstruction (MAME, kn5000_tonegen.cpp)

The decode of the bytecode above, rebuilt as audible DSP. Two parts: the parameter refresh (reads the decoded C-RAM coefficient cells once per update) and the per-sample insert (the signal processing). Default OFF, behind the DSPHLE research port. Source: src/mame/matsushita/kn5000_tonegen.cpp.

Parameter refresh (decode the C-RAM coefficients):

	const bool dist_hle = (dsphle == 15);
	double dist_drive = 0.0, dist_vol = 0.0;
	if (dist_hle)
	{
		auto q22d = [](u32 v) -> double {
			int32_t s = (v & 0x800000) ? int32_t(v) - 0x1000000 : int32_t(v);
			return double(s) / 4194304.0; };
		const bool bit1 = (dspcfg & 2) != 0;
		const double cs = bit1 ? 1.0 : 2.0;
		const double c0 = std::fabs(q22d(m_dsp1->cram_read(0x00)) * cs);   // DRIVE (measured)
		const double c2 = std::fabs(q22d(m_dsp1->cram_read(0x02)) * cs);   // VOLUME (measured)
		// Map the decoded DRIVE coeff (cell 0x00) to the shaper pre-gain. The AGC normalises the
		// signal toward unity, so this directly sets how hard it hits tanh: DISTORTION (curve A)
		// is the HARDEST clip of the family, so the floor clips aggressively (a near-square tone)
		// and a larger decoded DRIVE pushes it further toward a full square wave.
		dist_drive = std::clamp(4.0 + 9.0 * c0, 4.0, 13.0);
		// VOLUME: the decoded output-level, kept in a sane makeup range.
		dist_vol = std::clamp(0.25 + 0.75 * c2, 0.25, 1.0);
	}

Per-sample insert (the reconstructed signal path):

		if (dist_hle)
		{
			const double sl = double(mix_l) / 32768.0, sr = double(mix_r) / 32768.0;
			// AGC: a slow peak-envelope follower NORMALISES the signal to a roughly constant
			// level before the shaper (this is what an "AGC waveshaper" does -- it is why the
			// effect sustains and clips consistently regardless of how loud the note is). A
			// small floor keeps true silence silent (no noise pumping).
			m_dist_env_l = std::max(std::fabs(sl), m_dist_env_l * 0.9993);
			m_dist_env_r = std::max(std::fabs(sr), m_dist_env_r * 0.9993);
			const double norm_l = sl / (m_dist_env_l + 0.02);   // peak ~1 during a sustained note
			const double norm_r = sr / (m_dist_env_r + 0.02);
			// DRIVE (decoded) sets how hard the normalised signal hits the shaper; tanh is the
			// labelled SPECULATIVE clip curve. VOLUME (decoded) is the output makeup gain.
			const double wl = std::tanh(norm_l * dist_drive) * dist_vol;
			const double wr = std::tanh(norm_r * dist_drive) * dist_vol;
			mix_l = int32_t(std::clamp(wl, -1.0, 1.0) * 32767.0);
			mix_r = int32_t(std::clamp(wr, -1.0, 1.0) * 32767.0);
		}