PEQ + S.Delay — HLE reconstruction and DSP bytecode

DSPHLE selector 0x0d · program image prog72_peq_s_delay · Intervention-pinned (a panel control was driven and the moving C-RAM cell identified).

emphasis biquad (FREQ=0x03, GAIN=0x01) into a feedback delay.

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/prog72_peq_s_delay.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 -- PEQ+S.DELAY
; image rep algo 72  |  slots 72  |  unit 0 (I-RAM load 84)
; family combi  |  confidence high  |  54 words, 20 class-A multiplies (19 named)
; role: 1-band flat PEQ + single delay
; 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    088013000B   ?word   0x088013000B   ; 880.1.30.00B  hi12{ESC ?7 res=080}  [external delay-DRAM READ (FORCED, adjudication-round5 sect. 3 -- addr8 bit 6 is the direction field and 0x60 is the WRITE; this REVERSES R1 F1, which bounded the read latency to one repetition when the descriptors need twenty words); this end moves with the user's DELAY (ms) knob, and the delay is READ_CELL - WRITE_CELL; addr8 0x30 also marks the FIRST DRAM access of a body, 37 of 38 distinct images (R3 sect. 6.2). external delay-DRAM access; address = DESCRIPTOR_CELL[k] + G, from the host bank behind pointer ...825 / tag 0x4C (R3, PROVEN BY CONSTRUCTION) -- the k-th class-1 escape word of a body takes the k-th cell of that body's own descriptor block (the IDENTITY map, FORCED in adjudication-round5 sect. 1), so the address is NOT in this word]
  w1    000024B1CD   ld      (p),(p)+75
  w2    000020040E   ld      acc,(p)+0
  w3    0212200000   mac.b   (p)0,(p)+0 ; mem[p]<-acc, acc=0
  w4    0022A001D3   mac.ta  (p),c+,(p)+0
        ; C-RAM[0x00] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x00] = biquad b1 (role EQ, PROVEN)
  w5    0212A01412   mac     acc,c+,(p)+1 ; mem[p]<-acc, acc=0
        ; C-RAM[0x01] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x01] = biquad b0 (role EQ, PROVEN)
  w6    0202A011D5   mac     (p),c+,(p)+1
        ; C-RAM[0x02] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x02] = biquad b2 (role EQ, PROVEN)
  w7    0202A011D4   mac.tb  (p),c+,(p)+1
        ; C-RAM[0x03] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x03] = biquad -a1 (role EQ, PROVEN)
  w8    0202A001D5   mac     (p),c+,(p)+0
        ; C-RAM[0x04] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x04] = biquad -a2 (role EQ, PROVEN)
  w9    01022FF687   mac.st  tb,(p)-1
  w10   0804816415   post    acc,c
  w11   0212AFF407   mac.st  acc,c+,(p)-1 ; mem[p]<-acc, acc=0
        ; C-RAM[0x05] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x05] = biquad makeup (role EQ, PROVEN)
  w12   00002C1647   ld.st   ta,(p)-63
  w13   0000201407   ld.st   acc,(p)+1
  w14   0000A011D5   ld      (p),c+,(p)+1
        ; C-RAM[0x06] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x06] = filter section cell 0 (role filter, INFERRED)
  w15   0202AFF1D5   mac     (p),c+,(p)-1
        ; C-RAM[0x07] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x07] = filter section cell 1 (role filter, INFERRED)
  w16   08801602D9   dly.w  dsc[k],p+96
  w17   0202A01655   mac     ta,c+,(p)+1
        ; C-RAM[0x08] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x08] = filter section cell 2 (role filter, INFERRED)
  w18   00002FF000   nop
  w19   0212200419   mac.ta2 acc,(p)+0 ; mem[p]<-acc, acc=0
  w20   088012064B   ?word   0x088012064B   ; 880.1.20.64B  hi12{ESC ?7 res=080}  [external delay-DRAM READ (FORCED, adjudication-round5 sect. 3 -- addr8 bit 6 is the direction field and 0x60 is the WRITE; this REVERSES R1 F1, which bounded the read latency to one repetition when the descriptors need twenty words); this end moves with the user's DELAY (ms) knob, and the delay is READ_CELL - WRITE_CELL. external delay-DRAM access; address = DESCRIPTOR_CELL[k] + G, from the host bank behind pointer ...825 / tag 0x4C (R3, PROVEN BY CONSTRUCTION) -- the k-th class-1 escape word of a body takes the k-th cell of that body's own descriptor block (the IDENTITY map, FORCED in adjudication-round5 sect. 1), so the address is NOT in this word]
  w21   00002FF407   ld.st   acc,(p)-1
  w22   00002001D5   ld      (p),(p)+0
  w23   0202AF3415   mac     acc,c+,(p)-13
        ; C-RAM[0x09] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x09] = op0x66[0] (role mix/tap, INFERRED)
  w24   02042091CD   post    (p),(p)+9
  w25   00002F740E   ld      acc,(p)-9
  w26   021220A1CD   mac     (p),(p)+10 ; mem[p]<-acc, acc=0
  w27   00002FF1CE   ld      (p),(p)-1
  w28   0212246407   mac.st  acc,(p)+70 ; mem[p]<-acc, acc=0
  w29   0022200000   mac.b   (p)0,(p)+0
  w30   0000A001D3   ld.ta   (p),c+,(p)+0
        ; C-RAM[0x0A] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x0A] = biquad b1 (role EQ, PROVEN)
  w31   0212A01412   mac     acc,c+,(p)+1 ; mem[p]<-acc, acc=0
        ; C-RAM[0x0B] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x0B] = biquad b0 (role EQ, PROVEN)
  w32   0202A011D5   mac     (p),c+,(p)+1
        ; C-RAM[0x0C] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x0C] = biquad b2 (role EQ, PROVEN)
  w33   0202A011D4   mac.tb  (p),c+,(p)+1
        ; C-RAM[0x0D] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x0D] = biquad -a1 (role EQ, PROVEN)
  w34   0202A001D5   mac     (p),c+,(p)+0
        ; C-RAM[0x0E] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x0E] = biquad -a2 (role EQ, PROVEN)
  w35   01022FF687   mac.st  tb,(p)-1
  w36   0804816415   post    acc,c
  w37   0212AFF407   mac.st  acc,c+,(p)-1 ; mem[p]<-acc, acc=0
        ; C-RAM[0x0F] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x0F] = biquad makeup (role EQ, PROVEN)
  w38   00002BD647   ld.st   ta,(p)-67
  w39   0000201407   ld.st   acc,(p)+1
  w40   0000A011D5   ld      (p),c+,(p)+1
        ; C-RAM[0x10] (coeff, base 0x00 MEASURED)
  w41   0202A001D5   mac     (p),c+,(p)+0
        ; C-RAM[0x11] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x11] = filter section cell 0 (role filter, INFERRED)
  w42   08801602D9   dly.w  dsc[k],p+96
  w43   0202A00655   mac     ta,c+,(p)+0
        ; C-RAM[0x12] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x12] = filter section cell 1 (role filter, INFERRED)
  w44   0000200000   nop
  w45   0212200419   mac.ta2 acc,(p)+0 ; mem[p]<-acc, acc=0
  w46   088012064B   ?word   0x088012064B   ; 880.1.20.64B  hi12{ESC ?7 res=080}  [external delay-DRAM READ (FORCED, adjudication-round5 sect. 3 -- addr8 bit 6 is the direction field and 0x60 is the WRITE; this REVERSES R1 F1, which bounded the read latency to one repetition when the descriptors need twenty words); this end moves with the user's DELAY (ms) knob, and the delay is READ_CELL - WRITE_CELL. external delay-DRAM access; address = DESCRIPTOR_CELL[k] + G, from the host bank behind pointer ...825 / tag 0x4C (R3, PROVEN BY CONSTRUCTION) -- the k-th class-1 escape word of a body takes the k-th cell of that body's own descriptor block (the IDENTITY map, FORCED in adjudication-round5 sect. 1), so the address is NOT in this word]
  w47   00002FE407   ld.st   acc,(p)-2
  w48   00002001D5   ld      (p),(p)+0
  w49   0202AF3415   mac     acc,c+,(p)-13
        ; C-RAM[0x13] (coeff, base 0x00 MEASURED)
        ; coeff C-RAM[0x13] = op0x66[1] (role mix/tap, INFERRED)
  w50   02042FB1CD   post    (p),(p)-5
  w51   000020040E   ld      acc,(p)+0
  w52   0880160000   dly.w  dsc[k],p+96
  w53   061210E000   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 peqsd = (dsphle == 13);
	int    pq_dN = 0;
	double pq_gfb = 0.0;
	if (peqsd)
	{
		auto q22e = [](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;
		double c1 = q22e(m_dsp1->cram_read(0x01)) * cs;
		double c3 = q22e(m_dsp1->cram_read(0x03)) * cs;
		const double norm = (std::fabs(c3) <= 1.0) ? 2.0 : 1.0;   // operand -> cell scale
		c1 *= norm; c3 *= norm;
		const double cosw0 = std::clamp(c3 * 0.5, -0.9995, 0.9995);
		// per-band gain slope G by nearest decoded band centre (same table as the 5-band EQ)
		const double bc[5] = { 0.9954, 0.99055, 0.98, 0.95595, 0.89665 };
		const double bg[5] = { 465.8, 225.8, 114.2, 58.3, 30.5 };
		double G = bg[4]; double best = 1e9;
		for (int i = 0; i < 5; i++) { const double d = std::fabs(cosw0 - bc[i]); if (d < best) { best = d; G = bg[i]; } }
		const double asq = std::max(1.0 + G * (c1 - 0.5), 0.02);
		const double A = std::sqrt(asq), w0 = std::acos(cosw0), al = std::sin(w0) / 4.0;  // Q=2
		const double b0 = 1.0 + al * A, b1 = -2.0 * cosw0, b2 = 1.0 - al * A;
		const double a0 = 1.0 + al / A, a1 = -2.0 * cosw0, a2 = 1.0 - al / A;
		m_eq_l[0].set_coeffs(b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0, 1.0);
		m_eq_r[0].set_coeffs(b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0, 1.0);
		const double n441 = (bit1 ? 1.0 : 2.0) * double(m_dsp1->dsc_read(0x26));
		pq_dN = std::clamp(int(n441 * double(STREAM_RATE) / 44100.0 + 0.5), 1, 34999);
		pq_gfb = std::clamp(q22e(m_dsp1->cram_read(0x06)) * cs * 0.5, -0.95, 0.95);
	}

Per-sample insert (the reconstructed signal path):

		if (peqsd)
		{
			const double sl = double(mix_l) / 32768.0, sr = double(mix_r) / 32768.0;
			const double el = m_eq_l[0].process_one(sl), er = m_eq_r[0].process_one(sr);
			const double dtl = m_dly_l.read(double(pq_dN)), dtr = m_dly_r.read(double(pq_dN));
			m_dly_l.write(el + pq_gfb * dtl);
			m_dly_r.write(er + pq_gfb * dtr);
			mix_l = int32_t((0.5 * el + 0.5 * dtl) * 32768.0);
			mix_r = int32_t((0.5 * er + 0.5 * dtr) * 32768.0);
		}