Skip to content

DVI with Audio

A 1280x720 @ 30Hz DVI display with embedded audio. Four BRAM-backed tone generators play classical melodies as square wave PCM, mixed into a stereo stream and injected into DVI data island packets during horizontal blanking. An 80-bar spectrum analyzer with peak hold and color-gradient rendering provides real-time visualization on screen.

The design uses the same clock architecture as the DVI Color Bars example: 27 MHz crystal → 185.625 MHz serial clock (PLL) → 37.125 MHz pixel clock (CLKDIV).

Audio Pipeline

text
Binary melody files (track0-3.bin)
  └─ melodies (BRAM, 3-cycle read latency)
       └─ tone_gen ×4 (Bresenham 48kHz, square wave PCM)
            └─ mixer (4ch signed sum + DVI subpacket + BCH ECC)
                 └─ dvi_data_island (packet injection during hblank)

melodies

BRAM wrapper storing note data as pairs of 32-bit words (64 bits per note). Up to 1500 notes across 3 songs of 500 notes each. A phase-alternating read toggles between the two words of each note entry, latching word_hi and word_lo on alternate cycles. Fields extracted combinationally:

FieldBitsDescription
half_period16Square wave half-period in 48 kHz samples
duration24Note length in samples
gap16Articulation silence at end of note
volume8Amplitude (0-255)

A sentinel value (half_period = 0xFFFF) marks the end of each song for automatic loop-back.

tone_gen

Each instance reads from its own BRAM melody via OVERRIDE of a DATA_FILE constant. A Bresenham accumulator derives a 48 kHz sample clock from the 37.125 MHz pixel clock (step=16, threshold=12375). The tone generator toggles polarity at the note's half-period boundary to produce a square wave, applies volume scaling (left-shifted 3 bits for 8x gain), and runs the output through a 2-tap moving average filter for anti-aliasing. During the note's gap period, the output is zeroed for articulation.

A button press cycles between three songs by advancing song_base through offsets 0, 500, and 1000.

mixer

Purely combinational. Sums four signed 16-bit PCM channels using sadd for correct sign-extended widening, then formats the result as a DVI audio subpacket:

  • Left-justifies the 16-bit mix to 24 bits.
  • Computes even parity via XOR folding.
  • Builds status byte 6 with parity for both L and R channels (mono duplication).
  • Generates BCH(64,56) ECC using polynomial 0x83 in a combinational XOR network.

dvi_data_island

Injects four packet types into the 370-clock horizontal blanking interval:

text
Blanking period:
  ├─ Preamble:         8 clocks
  ├─ Leading guard:    2 clocks
  ├─ PKT0 (AVI):     32 clocks
  ├─ PKT1 (ACR):     32 clocks
  ├─ PKT2 (Audio):   32 clocks
  ├─ PKT3 (AIF):     32 clocks
  ├─ Trailing guard:   2 clocks
  └─ Control period

Shadow registers pre-load each packet's header and four 64-bit subpackets one cycle before transmission starts, reducing the bit-extraction mux to a single SELECT table. Audio samples are buffered between lines (2-3 samples per line at 48 kHz / ~22.5 kHz line rate).

The ACR packet carries N=6144 and CTS=37125, satisfying 48000 = 6144/37125 × 37,125,000.

terc4_encoder

Purely combinational 4-to-10-bit lookup table implementing the 16 TERC4 codewords from the DVI specification. Three instances encode the three data island channels.

Spectrum Analyzer

spectrum_analyzer

Maps each tone generator's current half-period to one of 80 log-spaced frequency bins (65 Hz to 5 kHz). For visual fullness, simulated 3rd and 5th harmonics are added with spectral spread:

  • Fundamental: full volume at center bar, half at ±1, quarter at ±2.
  • 3rd harmonic: 1/4 volume at center, 1/8 at ±1, 1/16 at ±2.
  • 5th harmonic: 1/8 volume at center, 1/16 at ±1.

Amplitudes are smoothed per frame with an exponential moving average and stored in DISTRIBUTED RAM alongside peak-hold values with a 4-bit decay timer.

spectrum_display

Renders 80 color-gradient bars with reflections. Each bar occupies a 16-pixel pitch (10px body + 6px gap). Bars grow upward from y=600 in segments of 16 pixels (12px body + 4px gap), up to 25 segments (400px maximum height). A reflection at 1/4 brightness extends below the baseline for up to 6 segments.

The color gradient runs from red (bar 0) through yellow (bar 39) to blue (bar 79), computed via a linear ramp approximation (half_pos × 13 >> 1).

Per-Instance Data with OVERRIDE

A single tone_gen module declares a DATA_FILE constant. Each of the four instances overrides it to point at a different binary file:

jz
@new tg0 tone_gen {
    OVERRIDE {
        DATA_FILE = "out/track0.bin";
    }
    IN  [1]  clk       = clk;
    IN  [1]  rst_n     = reset;
    ...
}

The OVERRIDE propagates through the module hierarchy — tone_gen passes DATA_FILE down to its melodies instance, which uses it in @file for BLOCK RAM initialization. The compiler generates separate BRAM contents for each instance.

Output Pipeline

The top-level module uses a two-stage output register pipeline for clean timing to the IO serializer. A per-cycle mux selects between four output modes:

  1. Data island + guard: channel 0 gets TERC4, channels 1-2 get guard pattern 0100110011.
  2. Data island active: all three channels get TERC4-encoded packet data.
  3. Video guard band: channels 0 and 2 get 1011001100, channel 1 gets 0100110011.
  4. Default: all channels get standard TMDS-encoded video/control data.

Build Process

bash
cd examples/dvi_audio
python3 tools/generate_melodies.py out/
make

The script produces track0.bin through track3.bin from musical notation, which are embedded into BLOCK RAM via @file during compilation.

jz
@project(CHIP="GW2AR-18-QN88-C8-I7") DVI_AUDIO_TEST
    @import "por.jz"
    @import "video_timing.jz"
    @import "tmds_encoder_2.jz"
    @import "tmds_encoder_10.jz"
    @import "terc4_encoder.jz"
    @import "dvi_data_island.jz"
    @import "melodies.jz"
    @import "tone_gen.jz"
    @import "mixer.jz"
    @import "spectrum_analyzer.jz"
    @import "spectrum_display.jz"
    @import "debounce.jz"
    @import "dvi.jz"

    CONFIG {
        serializer = 10;
    }
    @check(CONFIG.serializer == 2 || CONFIG.serializer == 10, "Serialzer must be 2 or 10.");

    CLOCKS {
        SCLK       = { period=37.037 }; // 27MHz crystal
        serial_clk;                      // 185.625MHz (5x pixel, from PLL)
        pixel_clk;                       // 37.125MHz (from CLKDIV)
    }

    IN_PINS {
        SCLK   = { standard=LVCMOS33 };
        DONE   = { standard=LVCMOS33 };
        KEY[2] = { standard=LVCMOS33 };
    }

    OUT_PINS {
        LED[6]       = { standard=LVCMOS33, drive=8 };
        TMDS_CLK     = { mode=DIFFERENTIAL, standard=LVDS25, drive=3.5, width=10, fclk=serial_clk, pclk=pixel_clk, reset=pll_lock };
        TMDS_DATA[3] = { mode=DIFFERENTIAL, standard=LVDS25, drive=3.5, width=10, fclk=serial_clk, pclk=pixel_clk, reset=pll_lock };
    }

    MAP {
        // System Clock 27MHz
        SCLK = 4;

        // Buttons (active high)
        KEY[0] = 87;
        KEY[1] = 88;

        // LEDs (active low)
        LED[0] = 15;
        LED[1] = 16;
        LED[2] = 17;
        LED[3] = 18;
        LED[4] = 19;
        LED[5] = 20;

        // DVI TMDS differential pairs
        TMDS_CLK     = { P=33, N=34 };
        TMDS_DATA[0] = { P=35, N=36 };
        TMDS_DATA[1] = { P=37, N=38 };
        TMDS_DATA[2] = { P=39, N=40 };

        // DONE (POR)
        DONE = IOR32B;
    }

    CLOCK_GEN {
        PLL {
            IN REF_CLK SCLK;
            OUT BASE   serial_clk;  // 185.625 MHz (5x pixel clock)
            WIRE LOCK  pll_lock;

            CONFIG {
                IDIV = 7;           // divider = 8
                FBDIV = 54;         // multiplier = 55
                ODIV = 4;           // VCO = 185.625 * 4 = 742.5 MHz
            };
        };
        CLKDIV {
            IN REF_CLK serial_clk;
            OUT BASE  pixel_clk;   // 185.625 / 5 = 37.125 MHz

            CONFIG {
                DIV_MODE = 5;
            };
        };
    }

    @top dvi_top {
        IN   [1]  clk       = pixel_clk;
        IN   [1]  por       = DONE;
        IN   [1]  rst_n     = ~KEY[1];
        IN   [1]  next      = KEY[0];
        OUT  [6]  leds      = ~LED;
        OUT  [10] tmds_clk  = TMDS_CLK;
        OUT  [10] tmds_d0   = TMDS_DATA[0];
        OUT  [10] tmds_d1   = TMDS_DATA[1];
        OUT  [10] tmds_d2   = TMDS_DATA[2];
    }
@endproj
jz
// DVI 1280x720 @ 30Hz Vertical Color Bar with Audio
// Outputs R/G/B/White/Black horizontal bars (by y_pos).
// Uses TMDS encoding for DVI output with Data Island packets
// (AVI InfoFrame, ACR, Audio Sample, Audio InfoFrame) during blanking.
// Audio: 4-track Ode to Joy, square wave, 2ch 16-bit L-PCM at 48kHz.
//
// All TMDS outputs go through a registered mux in the SYNCHRONOUS block
// to give the OSER10 a clean register-to-primitive path.
@module dvi_top
    PORT {
        IN   [1]  clk;         // pixel_clk (37.125 MHz)
        IN   [1]  por;         // POR input from DONE
        IN   [1]  rst_n;       // Active-low reset from button
        IN   [1]  next;        // Next song button (active-low)
        OUT  [6]  leds;        // Status LEDs
        OUT  [10] tmds_clk;    // TMDS clock channel (serialized)
        OUT  [10] tmds_d0;     // TMDS data channel 0 (blue)
        OUT  [10] tmds_d1;     // TMDS data channel 1 (green)
        OUT  [10] tmds_d2;     // TMDS data channel 2 (red)
    }

    WIRE {
        reset       [1];
        por_n       [1];
        hsync       [1];
        vsync       [1];
        de          [1];
        x_pos       [11];
        y_pos       [10];
        red         [8];
        green       [8];
        blue        [8];

        // DVI TMDS encoder outputs
        dvi_tmds_d0 [10];
        dvi_tmds_d1 [10];
        dvi_tmds_d2 [10];

        // TERC4 encoder outputs
        terc4_ch0_data [4];
        terc4_ch1_data [4];
        terc4_ch2_data [4];
        terc4_d0       [10];
        terc4_d1       [10];
        terc4_d2       [10];

        // Data island control
        di_active      [1];
        di_preamble    [1];
        di_guard       [1];

        // DVI encoder control signals
        enc1_c0        [1];
        enc1_c1        [1];
        enc2_c0        [1];
        enc2_c1        [1];

        // Video preamble and guard band (combinational)
        video_preamble_pre [1];
        video_guard_pre    [1];

        // Tone generator PCM outputs
        tg0_sample    [16];
        tg0_valid     [1];
        tg1_sample    [16];
        tg1_valid     [1];
        tg2_sample    [16];
        tg2_valid     [1];
        tg3_sample    [16];
        tg3_valid     [1];

        // Tone generator audio state outputs
        tg0_half_period [16];
        tg0_volume      [8];
        tg0_in_gap      [1];
        tg1_half_period [16];
        tg1_volume      [8];
        tg1_in_gap      [1];
        tg2_half_period [16];
        tg2_volume      [8];
        tg2_in_gap      [1];
        tg3_half_period [16];
        tg3_volume      [8];
        tg3_in_gap      [1];

        // Frame pulse for spectrum analyzer
        frame_pulse     [1];

        // Button debounce
        next_press      [1];

        // Spectrum analyzer <-> display interconnect
        sp_rd_bar       [7];
        sp_rd_amp       [16];

        // Mixer outputs
        mix_samp_lo   [32];
        mix_samp_hi   [32];
        mix_valid     [1];
    }

    REGISTER {
        heartbeat_cnt [25] = 25'b0;
        heartbeat_led [1]  = 1'b0;

        // Data island pipeline (1 cycle to align TERC4 with encoder output)
        di_active_r  [1]  = 1'b0;
        di_guard_r   [1]  = 1'b0;
        terc4_d0_r   [10] = 10'd0;
        terc4_d1_r   [10] = 10'd0;
        terc4_d2_r   [10] = 10'd0;

        // Video preamble and guard band
        video_preamble_r [1] = 1'b0;
        video_guard_r    [1] = 1'b0;

        // Two-stage TMDS output pipeline:
        // Stage 1 (pre): mux logic settles here
        tmds_d0_pre  [10] = 10'd0;
        tmds_d1_pre  [10] = 10'd0;
        tmds_d2_pre  [10] = 10'd0;

        // Stage 2 (r): simple copy, clean FF-to-OSER10 path
        tmds_d0_r    [10] = 10'd0;
        tmds_d1_r    [10] = 10'd0;
        tmds_d2_r    [10] = 10'd0;
    }

    @new por0 por {
        IN  [1] clk   = clk;
        IN  [1] done  = por;
        OUT [1] por_n = por_n;
    }

    @new db0 debounce {
        IN  [1] clk       = clk;
        IN  [1] rst_n     = reset;
        IN  [1] btn_in    = next;
        OUT [1] btn_press = next_press;
    }

    @new vt0 video_timing {
        IN  [1]  clk            = clk;
        IN  [1]  rst_n          = reset;
        OUT [1]  hsync          = hsync;
        OUT [1]  vsync          = vsync;
        OUT [1]  display_enable = de;
        OUT [11] x_pos          = x_pos;
        OUT [10] y_pos          = y_pos;
    }

    @feature CONFIG.serializer == 10
        // Blue channel (data channel 0) - carries sync signals
        @new enc0 tmds_encoder_10 {
            IN  [1]  clk            = clk;
            IN  [1]  rst_n          = reset;
            IN  [8]  data_in        = blue;
            IN  [1]  c0             = hsync;
            IN  [1]  c1             = vsync;
            IN  [1]  display_enable = de;
            OUT [10] tmds_out       = dvi_tmds_d0;
        }

        // Green channel (data channel 1) - preamble CTL on c0/c1
        @new enc1 tmds_encoder_10 {
            IN  [1]  clk            = clk;
            IN  [1]  rst_n          = reset;
            IN  [8]  data_in        = green;
            IN  [1]  c0             = enc1_c0;
            IN  [1]  c1             = enc1_c1;
            IN  [1]  display_enable = de;
            OUT [10] tmds_out       = dvi_tmds_d1;
        }

        // Red channel (data channel 2) - preamble CTL on c0/c1
        @new enc2 tmds_encoder_10 {
            IN  [1]  clk            = clk;
            IN  [1]  rst_n          = reset;
            IN  [8]  data_in        = red;
            IN  [1]  c0             = enc2_c0;
            IN  [1]  c1             = enc2_c1;
            IN  [1]  display_enable = de;
            OUT [10] tmds_out       = dvi_tmds_d2;
        }
    @else
    @endfeat

    // TERC4 encoders for data island period
    @new t4_0 terc4_encoder {
        IN  [4]  data_in   = terc4_ch0_data;
        OUT [10] terc4_out = terc4_d0;
    }

    @new t4_1 terc4_encoder {
        IN  [4]  data_in   = terc4_ch1_data;
        OUT [10] terc4_out = terc4_d1;
    }

    @new t4_2 terc4_encoder {
        IN  [4]  data_in   = terc4_ch2_data;
        OUT [10] terc4_out = terc4_d2;
    }

    // Tone generators (4 independent tracks)
    @new tg0 tone_gen {
        OVERRIDE {
            DATA_FILE = "../out/track0.bin";
        }
        IN  [1]  clk            = clk;
        IN  [1]  rst_n          = reset;
        IN  [1]  next_song      = next_press;
        OUT [16] sample         = tg0_sample;
        OUT [1]  samp_valid     = tg0_valid;
        OUT [16] half_period_out = tg0_half_period;
        OUT [8]  volume_out     = tg0_volume;
        OUT [1]  in_gap_out     = tg0_in_gap;
    }

    @new tg1 tone_gen {
        OVERRIDE {
            DATA_FILE = "../out/track1.bin";
        }
        IN  [1]  clk            = clk;
        IN  [1]  rst_n          = reset;
        IN  [1]  next_song      = next_press;
        OUT [16] sample         = tg1_sample;
        OUT [1]  samp_valid     = tg1_valid;
        OUT [16] half_period_out = tg1_half_period;
        OUT [8]  volume_out     = tg1_volume;
        OUT [1]  in_gap_out     = tg1_in_gap;
    }

    @new tg2 tone_gen {
        OVERRIDE {
            DATA_FILE = "../out/track2.bin";
        }
        IN  [1]  clk            = clk;
        IN  [1]  rst_n          = reset;
        IN  [1]  next_song      = next_press;
        OUT [16] sample         = tg2_sample;
        OUT [1]  samp_valid     = tg2_valid;
        OUT [16] half_period_out = tg2_half_period;
        OUT [8]  volume_out     = tg2_volume;
        OUT [1]  in_gap_out     = tg2_in_gap;
    }

    @new tg3 tone_gen {
        OVERRIDE {
            DATA_FILE = "../out/track3.bin";
        }
        IN  [1]  clk            = clk;
        IN  [1]  rst_n          = reset;
        IN  [1]  next_song      = next_press;
        OUT [16] sample         = tg3_sample;
        OUT [1]  samp_valid     = tg3_valid;
        OUT [16] half_period_out = tg3_half_period;
        OUT [8]  volume_out     = tg3_volume;
        OUT [1]  in_gap_out     = tg3_in_gap;
    }

    // 4-channel mixer + DVI subpacket formatter
    @new mx0 mixer {
        IN  [16] s0         = tg0_sample;
        IN  [16] s1         = tg1_sample;
        IN  [16] s2         = tg2_sample;
        IN  [16] s3         = tg3_sample;
        IN  [1]  samp_valid = tg0_valid;
        OUT [32] samp_lo    = mix_samp_lo;
        OUT [32] samp_hi    = mix_samp_hi;
        OUT [1]  out_valid  = mix_valid;
    }

    // DVI data island controller (AVI + ACR + Audio Sample + Audio InfoFrame)
    @new di0 dvi_data_island {
        IN  [1]  clk               = clk;
        IN  [1]  rst_n             = reset;
        IN  [1]  hsync             = hsync;
        IN  [1]  vsync             = vsync;
        IN  [1]  display_enable    = de;
        IN  [11] x_pos             = x_pos;
        IN  [32] samp_lo           = mix_samp_lo;
        IN  [32] samp_hi           = mix_samp_hi;
        IN  [1]  samp_valid        = mix_valid;
        OUT [4]  terc4_ch0         = terc4_ch0_data;
        OUT [4]  terc4_ch1         = terc4_ch1_data;
        OUT [4]  terc4_ch2         = terc4_ch2_data;
        OUT [1]  data_island_active = di_active;
        OUT [1]  preamble_active   = di_preamble;
        OUT [1]  guard_active      = di_guard;
    }

    @new sa0 spectrum_analyzer {
        IN  [1]  clk            = clk;
        IN  [1]  rst_n          = reset;
        IN  [16] ch0_half_period = tg0_half_period;
        IN  [8]  ch0_volume     = tg0_volume;
        IN  [1]  ch0_in_gap     = tg0_in_gap;
        IN  [16] ch1_half_period = tg1_half_period;
        IN  [8]  ch1_volume     = tg1_volume;
        IN  [1]  ch1_in_gap     = tg1_in_gap;
        IN  [16] ch2_half_period = tg2_half_period;
        IN  [8]  ch2_volume     = tg2_volume;
        IN  [1]  ch2_in_gap     = tg2_in_gap;
        IN  [16] ch3_half_period = tg3_half_period;
        IN  [8]  ch3_volume     = tg3_volume;
        IN  [1]  ch3_in_gap     = tg3_in_gap;
        IN  [1]  frame_pulse    = frame_pulse;
        IN  [7]  rd_bar         = sp_rd_bar;
        OUT [16] rd_amp         = sp_rd_amp;
    }

    @new sd0 spectrum_display {
        IN  [1]  clk     = clk;
        IN  [1]  rst_n   = reset;
        IN  [11] x_pos   = x_pos;
        IN  [10] y_pos   = y_pos;
        OUT [7]  rd_bar   = sp_rd_bar;
        IN  [16] rd_amp   = sp_rd_amp;
        OUT [8]  red     = red;
        OUT [8]  green   = green;
        OUT [8]  blue    = blue;
    }

    ASYNCHRONOUS {
        reset <= rst_n & por_n;

        // Frame pulse: high for 1 cycle at top-left pixel (start of each frame)
        frame_pulse <= (x_pos == 11'd0 && y_pos == 10'd0) ? 1'b1 : 1'b0;

        // TMDS clock channel: fixed 1111100000 pattern
        tmds_clk <= 10'b1111100000;

        // Registered TMDS output to port
        tmds_d0 <= tmds_d0_r;
        tmds_d1 <= tmds_d1_r;
        tmds_d2 <= tmds_d2_r;

        // Preamble CTL signals on ch1/ch2
        //   Data island preamble: ch1 c0=1,c1=0; ch2 c0=1,c1=0
        //   Video preamble:       ch1 c0=1,c1=0; ch2 c0=0,c1=0
        IF (di_preamble == 1'b1) {
            enc1_c0 <= 1'b1;
            enc1_c1 <= 1'b0;
            enc2_c0 <= 1'b1;
            enc2_c1 <= 1'b0;
        } ELIF (video_preamble_r == 1'b1) {
            enc1_c0 <= 1'b1;
            enc1_c1 <= 1'b0;
            enc2_c0 <= 1'b0;
            enc2_c1 <= 1'b0;
        } ELSE {
            enc1_c0 <= 1'b0;
            enc1_c1 <= 1'b0;
            enc2_c0 <= 1'b0;
            enc2_c1 <= 1'b0;
        }

        // Video preamble: 4-cycle pipeline (preamble_r + CTL -> encoder + output reg + OSER)
        video_preamble_pre <= (
            x_pos >= 11'd1640 && x_pos < 11'd1648 &&
            (y_pos < 10'd719 || y_pos == 10'd749)
        ) ? 1'b1 : 1'b0;

        // Video guard band: 3-cycle pipeline (guard_r + mux reg + OSER)
        video_guard_pre <= (
            (x_pos == 11'd1648 || x_pos == 11'd1649) &&
            (y_pos < 10'd719 || y_pos == 10'd749)
        ) ? 1'b1 : 1'b0;

        // LED status
        leds <= { heartbeat_led, 5'b00000 };
    }

    SYNCHRONOUS(CLK=clk RESET=reset RESET_ACTIVE=Low) {
        // Registration pipeline
        di_active_r      <= di_active;
        di_guard_r       <= di_guard;
        terc4_d0_r       <= terc4_d0;
        terc4_d1_r       <= terc4_d1;
        terc4_d2_r       <= terc4_d2;
        video_preamble_r <= video_preamble_pre;
        video_guard_r    <= video_guard_pre;

        // Output mux: select between data island TERC4, video guard, and DVI TMDS
        IF (di_active_r == 1'b1) {
            IF (di_guard_r == 1'b1) {
                // DI guard band: ch0=TERC4, ch1/ch2=fixed pattern
                tmds_d0_pre <= terc4_d0_r;
                tmds_d1_pre <= 10'b0100110011;
                tmds_d2_pre <= 10'b0100110011;
            } ELSE {
                // DI packet data: all channels TERC4
                tmds_d0_pre <= terc4_d0_r;
                tmds_d1_pre <= terc4_d1_r;
                tmds_d2_pre <= terc4_d2_r;
            }
        } ELIF (video_guard_r == 1'b1) {
            // Video guard band (DVI 1.0 / HDMI 1.4a S5.2.2)
            tmds_d0_pre <= 10'b1011001100;
            tmds_d1_pre <= 10'b0100110011;
            tmds_d2_pre <= 10'b1011001100;
        } ELSE {
            // Control/preamble/video: DVI TMDS
            tmds_d0_pre <= dvi_tmds_d0;
            tmds_d1_pre <= dvi_tmds_d1;
            tmds_d2_pre <= dvi_tmds_d2;
        }

        // Stage 2: Simple copy → clean FF-to-OSER10 path
        tmds_d0_r <= tmds_d0_pre;
        tmds_d1_r <= tmds_d1_pre;
        tmds_d2_r <= tmds_d2_pre;

        // Heartbeat blinker
        IF (heartbeat_cnt == 25'd33_554_431) {
            heartbeat_cnt <= 25'b0;
            heartbeat_led <= ~heartbeat_led;
        } ELSE {
            heartbeat_cnt <= heartbeat_cnt + 25'b1;
        }
    }
@endmod
jz
// 1280x720 @ 30Hz Video Timing Generator
// CEA-861 timings, pixel clock = 37.125 MHz (half of 74.25 MHz)
// H total: 1650 (1280 active + 110 front + 40 sync + 220 back)
// V total: 750  (720 active + 5 front + 5 sync + 20 back)
// Sync polarity: positive (sync HIGH during sync pulse)
@module video_timing
    PORT {
        IN  [1]  clk;
        IN  [1]  rst_n;
        OUT [1]  hsync;
        OUT [1]  vsync;
        OUT [1]  display_enable;
        OUT [11] x_pos;
        OUT [10] y_pos;
    }

    CONST {
        // Horizontal timing
        H_ACTIVE = 1280;
        H_FRONT  = 110;
        H_SYNC   = 40;
        H_BACK   = 220;
        H_TOTAL  = 1650;

        // Vertical timing
        V_ACTIVE = 720;
        V_FRONT  = 5;
        V_SYNC   = 5;
        V_BACK   = 20;
        V_TOTAL  = 750;
    }

    REGISTER {
        h_cnt [11] = 11'b0;
        v_cnt [10] = 10'b0;
    }

    ASYNCHRONOUS {
        // Positive sync polarity: HIGH during sync pulse, LOW otherwise
        hsync <= (h_cnt >= lit(11, H_ACTIVE + H_FRONT) &&
                  h_cnt <  lit(11, H_ACTIVE + H_FRONT + H_SYNC))
                 ? 1'b1 : 1'b0;

        vsync <= (v_cnt >= lit(10, V_ACTIVE + V_FRONT) &&
                  v_cnt <  lit(10, V_ACTIVE + V_FRONT + V_SYNC))
                 ? 1'b1 : 1'b0;

        // Display enable: active region
        display_enable <= (h_cnt < lit(11, H_ACTIVE) &&
                           v_cnt < lit(10, V_ACTIVE))
                          ? 1'b1 : 1'b0;

        x_pos <= h_cnt;
        y_pos <= v_cnt;
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        IF (h_cnt == lit(11, H_TOTAL - 1)) {
            h_cnt <= 11'b0;
            IF (v_cnt == lit(10, V_TOTAL - 1)) {
                v_cnt <= 10'b0;
            } ELSE {
                v_cnt <= v_cnt + 10'b1;
            }
        } ELSE {
            h_cnt <= h_cnt + 11'b1;
        }
    }
@endmod
jz
// DVI TMDS 8b/10b Encoder
// Full DVI-compliant TMDS encoding with XOR/XNOR selection and
// running disparity tracking for DC balance on AC-coupled links.
@module tmds_encoder_10
    PORT {
        IN  [1]  clk;
        IN  [1]  rst_n;
        IN  [8]  data_in;
        IN  [1]  c0;
        IN  [1]  c1;
        IN  [1]  display_enable;
        OUT [10] tmds_out;
    }

    WIRE {
        // Popcount of data_in (adder tree)
        d_p0 [2]; d_p1 [2]; d_p2 [2]; d_p3 [2];
        d_s0 [3]; d_s1 [3];
        n1_d [4];

        // XOR/XNOR mode selection
        use_xnor [1];

        // Transition-minimized intermediate word q_m[8:0]
        qm0 [1]; qm1 [1]; qm2 [1]; qm3 [1];
        qm4 [1]; qm5 [1]; qm6 [1]; qm7 [1];
        qm8 [1];

        // Popcount of q_m[7:0] (adder tree)
        q_p0 [2]; q_p1 [2]; q_p2 [2]; q_p3 [2];
        q_s0 [3]; q_s1 [3];
        n1_q [4];

        // Disparity conditions
        cnt_is_zero [1];
        qm_balanced [1];
        cond1       [1];
        cnt_sign    [1];
        cond_inv    [1];

        // Arithmetic for disparity update (5-bit two's complement)
        diff_n1n0 [5];
        diff_n0n1 [5];
        qm8_x2    [5];
        nqm8_x2   [5];

        // Combinational outputs
        tmds_data [10];
        next_cnt  [5];
    }

    REGISTER {
        cnt      [5]  = 5'b00000;
        tmds_reg [10] = 10'b0000000000;
    }

    ASYNCHRONOUS {
        tmds_out <= tmds_reg;

        // --- Popcount of data_in ---
        d_p0 <= {1'b0, data_in[0]} + {1'b0, data_in[1]};
        d_p1 <= {1'b0, data_in[2]} + {1'b0, data_in[3]};
        d_p2 <= {1'b0, data_in[4]} + {1'b0, data_in[5]};
        d_p3 <= {1'b0, data_in[6]} + {1'b0, data_in[7]};
        d_s0 <= {1'b0, d_p0} + {1'b0, d_p1};
        d_s1 <= {1'b0, d_p2} + {1'b0, d_p3};
        n1_d <= {1'b0, d_s0} + {1'b0, d_s1};

        // --- XOR/XNOR selection (DVI spec section 3.3.1) ---
        use_xnor <= (n1_d > 4'd4 || (n1_d == 4'd4 && data_in[0] == 1'b0))
                     ? 1'b1 : 1'b0;

        // --- Build transition-minimized word q_m ---
        qm0 <= data_in[0];
        qm1 <= (use_xnor == 1'b1) ? ~(data_in[1] ^ qm0) : (data_in[1] ^ qm0);
        qm2 <= (use_xnor == 1'b1) ? ~(data_in[2] ^ qm1) : (data_in[2] ^ qm1);
        qm3 <= (use_xnor == 1'b1) ? ~(data_in[3] ^ qm2) : (data_in[3] ^ qm2);
        qm4 <= (use_xnor == 1'b1) ? ~(data_in[4] ^ qm3) : (data_in[4] ^ qm3);
        qm5 <= (use_xnor == 1'b1) ? ~(data_in[5] ^ qm4) : (data_in[5] ^ qm4);
        qm6 <= (use_xnor == 1'b1) ? ~(data_in[6] ^ qm5) : (data_in[6] ^ qm5);
        qm7 <= (use_xnor == 1'b1) ? ~(data_in[7] ^ qm6) : (data_in[7] ^ qm6);
        qm8 <= (use_xnor == 1'b1) ? 1'b0 : 1'b1;

        // --- Popcount of q_m[7:0] ---
        q_p0 <= {1'b0, qm0} + {1'b0, qm1};
        q_p1 <= {1'b0, qm2} + {1'b0, qm3};
        q_p2 <= {1'b0, qm4} + {1'b0, qm5};
        q_p3 <= {1'b0, qm6} + {1'b0, qm7};
        q_s0 <= {1'b0, q_p0} + {1'b0, q_p1};
        q_s1 <= {1'b0, q_p2} + {1'b0, q_p3};
        n1_q <= {1'b0, q_s0} + {1'b0, q_s1};

        // --- Disparity conditions ---
        cnt_is_zero <= (cnt == 5'b00000) ? 1'b1 : 1'b0;
        qm_balanced <= (n1_q == 4'd4) ? 1'b1 : 1'b0;
        cond1       <= (cnt_is_zero == 1'b1 || qm_balanced == 1'b1)
                        ? 1'b1 : 1'b0;
        cnt_sign    <= cnt[4];
        cond_inv    <= ((cnt_sign == 1'b0 && cnt_is_zero == 1'b0 && n1_q > 4'd4) ||
                        (cnt_sign == 1'b1 && n1_q < 4'd4))
                       ? 1'b1 : 1'b0;

        // --- Arithmetic helpers (5-bit two's complement) ---
        diff_n1n0 <= {n1_q, 1'b0} - 5'd8;
        diff_n0n1 <= 5'd8 - {n1_q, 1'b0};
        qm8_x2   <= {3'b000, qm8, 1'b0};
        nqm8_x2  <= {3'b000, ~qm8, 1'b0};

        // --- Output word and next disparity (DVI spec section 3.3.2) ---
        IF (cond1 == 1'b1) {
            IF (qm8 == 1'b0) {
                // XNOR mode, cnt==0 or balanced: invert data, bit[9]=1
                tmds_data <= {1'b1, 1'b0, ~qm7, ~qm6, ~qm5, ~qm4,
                              ~qm3, ~qm2, ~qm1, ~qm0};
                next_cnt  <= cnt + diff_n0n1;
            } ELSE {
                // XOR mode, cnt==0 or balanced: keep data, bit[9]=0
                tmds_data <= {1'b0, 1'b1, qm7, qm6, qm5, qm4,
                              qm3, qm2, qm1, qm0};
                next_cnt  <= cnt + diff_n1n0;
            }
        } ELIF (cond_inv == 1'b1) {
            // Invert to reduce disparity
            tmds_data <= {1'b1, qm8, ~qm7, ~qm6, ~qm5, ~qm4,
                          ~qm3, ~qm2, ~qm1, ~qm0};
            next_cnt  <= cnt + qm8_x2 + diff_n0n1;
        } ELSE {
            // Don't invert
            tmds_data <= {1'b0, qm8, qm7, qm6, qm5, qm4,
                          qm3, qm2, qm1, qm0};
            next_cnt  <= cnt - nqm8_x2 + diff_n1n0;
        }
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        IF (display_enable == 1'b0) {
            // Control period: reset disparity and emit control tokens
            cnt <= 5'b00000;
            IF (c0 == 1'b0 && c1 == 1'b0) {
                tmds_reg <= 10'b1101010100;
            } ELIF (c0 == 1'b1 && c1 == 1'b0) {
                tmds_reg <= 10'b0010101011;
            } ELIF (c0 == 1'b0 && c1 == 1'b1) {
                tmds_reg <= 10'b0101010100;
            } ELSE {
                tmds_reg <= 10'b1010101011;
            }
        } ELSE {
            // Data period: latch encoded word and update disparity
            tmds_reg <= tmds_data;
            cnt <= next_cnt;
        }
    }
@endmod
jz
// DVI TMDS 8b/10b Encoder with internal 2-bit shift register
// Same TMDS encoding and PORT as tmds_encoder_10. The 10-bit encoded
// word is stored in a register and output on tmds_out[10] exactly like
// the _10 variant. Internally, a parallel shift register also serializes
// the word 2 bits at a time — this output is exposed as serial_d[2] for
// connection to a 2:1 DDR primitive (e.g., ODDRX1F on ECP5).
//
// clk = 5x pixel clock (drives the shift register)
// The TMDS encoding and disparity tracking run at pixel rate by gating
// on shift_cnt == 0 (once every 5 serial clocks).
@module tmds_encoder_2
    PORT {
        IN  [1]  clk;
        IN  [1]  rst_n;
        IN  [8]  data_in;
        IN  [1]  c0;
        IN  [1]  c1;
        IN  [1]  display_enable;
        OUT [10] tmds_out;
    }

    WIRE {
        // Popcount of data_in (adder tree)
        d_p0 [2]; d_p1 [2]; d_p2 [2]; d_p3 [2];
        d_s0 [3]; d_s1 [3];
        n1_d [4];

        // XOR/XNOR mode selection
        use_xnor [1];

        // Transition-minimized intermediate word q_m[8:0]
        qm0 [1]; qm1 [1]; qm2 [1]; qm3 [1];
        qm4 [1]; qm5 [1]; qm6 [1]; qm7 [1];
        qm8 [1];

        // Popcount of q_m[7:0] (adder tree)
        q_p0 [2]; q_p1 [2]; q_p2 [2]; q_p3 [2];
        q_s0 [3]; q_s1 [3];
        n1_q [4];

        // Disparity conditions
        cnt_is_zero [1];
        qm_balanced [1];
        cond1       [1];
        cnt_sign    [1];
        cond_inv    [1];

        // Arithmetic for disparity update (5-bit two's complement)
        diff_n1n0 [5];
        diff_n0n1 [5];
        qm8_x2    [5];
        nqm8_x2   [5];

        // Combinational TMDS encoded word
        tmds_data [10];
        next_cnt  [5];
    }

    REGISTER {
        cnt       [5]  = 5'b00000;
        tmds_reg  [10] = 10'b0000000000;

        // Internal shift register for 2-bit serialization
        shift_reg [10] = 10'b0000000000;
        shift_cnt [3]  = 3'b000;
    }

    ASYNCHRONOUS {
        // Output the full 10-bit encoded word (same as tmds_encoder_10)
        tmds_out <= tmds_reg;

        // --- Popcount of data_in ---
        d_p0 <= {1'b0, data_in[0]} + {1'b0, data_in[1]};
        d_p1 <= {1'b0, data_in[2]} + {1'b0, data_in[3]};
        d_p2 <= {1'b0, data_in[4]} + {1'b0, data_in[5]};
        d_p3 <= {1'b0, data_in[6]} + {1'b0, data_in[7]};
        d_s0 <= {1'b0, d_p0} + {1'b0, d_p1};
        d_s1 <= {1'b0, d_p2} + {1'b0, d_p3};
        n1_d <= {1'b0, d_s0} + {1'b0, d_s1};

        // --- XOR/XNOR selection (DVI spec section 3.3.1) ---
        use_xnor <= (n1_d > 4'd4 || (n1_d == 4'd4 && data_in[0] == 1'b0))
                     ? 1'b1 : 1'b0;

        // --- Build transition-minimized word q_m ---
        qm0 <= data_in[0];
        qm1 <= (use_xnor == 1'b1) ? ~(data_in[1] ^ qm0) : (data_in[1] ^ qm0);
        qm2 <= (use_xnor == 1'b1) ? ~(data_in[2] ^ qm1) : (data_in[2] ^ qm1);
        qm3 <= (use_xnor == 1'b1) ? ~(data_in[3] ^ qm2) : (data_in[3] ^ qm2);
        qm4 <= (use_xnor == 1'b1) ? ~(data_in[4] ^ qm3) : (data_in[4] ^ qm3);
        qm5 <= (use_xnor == 1'b1) ? ~(data_in[5] ^ qm4) : (data_in[5] ^ qm4);
        qm6 <= (use_xnor == 1'b1) ? ~(data_in[6] ^ qm5) : (data_in[6] ^ qm5);
        qm7 <= (use_xnor == 1'b1) ? ~(data_in[7] ^ qm6) : (data_in[7] ^ qm6);
        qm8 <= (use_xnor == 1'b1) ? 1'b0 : 1'b1;

        // --- Popcount of q_m[7:0] ---
        q_p0 <= {1'b0, qm0} + {1'b0, qm1};
        q_p1 <= {1'b0, qm2} + {1'b0, qm3};
        q_p2 <= {1'b0, qm4} + {1'b0, qm5};
        q_p3 <= {1'b0, qm6} + {1'b0, qm7};
        q_s0 <= {1'b0, q_p0} + {1'b0, q_p1};
        q_s1 <= {1'b0, q_p2} + {1'b0, q_p3};
        n1_q <= {1'b0, q_s0} + {1'b0, q_s1};

        // --- Disparity conditions ---
        cnt_is_zero <= (cnt == 5'b00000) ? 1'b1 : 1'b0;
        qm_balanced <= (n1_q == 4'd4) ? 1'b1 : 1'b0;
        cond1       <= (cnt_is_zero == 1'b1 || qm_balanced == 1'b1)
                        ? 1'b1 : 1'b0;
        cnt_sign    <= cnt[4];
        cond_inv    <= ((cnt_sign == 1'b0 && cnt_is_zero == 1'b0 && n1_q > 4'd4) ||
                        (cnt_sign == 1'b1 && n1_q < 4'd4))
                       ? 1'b1 : 1'b0;

        // --- Arithmetic helpers (5-bit two's complement) ---
        diff_n1n0 <= {n1_q, 1'b0} - 5'd8;
        diff_n0n1 <= 5'd8 - {n1_q, 1'b0};
        qm8_x2   <= {3'b000, qm8, 1'b0};
        nqm8_x2  <= {3'b000, ~qm8, 1'b0};

        // --- Output word and next disparity (DVI spec section 3.3.2) ---
        IF (cond1 == 1'b1) {
            IF (qm8 == 1'b0) {
                tmds_data <= {1'b1, 1'b0, ~qm7, ~qm6, ~qm5, ~qm4,
                              ~qm3, ~qm2, ~qm1, ~qm0};
                next_cnt  <= cnt + diff_n0n1;
            } ELSE {
                tmds_data <= {1'b0, 1'b1, qm7, qm6, qm5, qm4,
                              qm3, qm2, qm1, qm0};
                next_cnt  <= cnt + diff_n1n0;
            }
        } ELIF (cond_inv == 1'b1) {
            tmds_data <= {1'b1, qm8, ~qm7, ~qm6, ~qm5, ~qm4,
                          ~qm3, ~qm2, ~qm1, ~qm0};
            next_cnt  <= cnt + qm8_x2 + diff_n0n1;
        } ELSE {
            tmds_data <= {1'b0, qm8, qm7, qm6, qm5, qm4,
                          qm3, qm2, qm1, qm0};
            next_cnt  <= cnt - nqm8_x2 + diff_n1n0;
        }
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        IF (shift_cnt == 3'd4) {
            // Every 5th serial clock: load new word, update encoding
            shift_cnt <= 3'd0;

            IF (display_enable == 1'b0) {
                cnt <= 5'b00000;
                IF (c0 == 1'b0 && c1 == 1'b0) {
                    tmds_reg  <= 10'b1101010100;
                    shift_reg <= 10'b1101010100;
                } ELIF (c0 == 1'b1 && c1 == 1'b0) {
                    tmds_reg  <= 10'b0010101011;
                    shift_reg <= 10'b0010101011;
                } ELIF (c0 == 1'b0 && c1 == 1'b1) {
                    tmds_reg  <= 10'b0101010100;
                    shift_reg <= 10'b0101010100;
                } ELSE {
                    tmds_reg  <= 10'b1010101011;
                    shift_reg <= 10'b1010101011;
                }
            } ELSE {
                tmds_reg  <= tmds_data;
                shift_reg <= tmds_data;
                cnt <= next_cnt;
            }
        } ELSE {
            // Shift out 2 bits: shift right by 2
            shift_reg <= {2'b00, shift_reg[9:2]};
            shift_cnt <= shift_cnt + 3'd1;
        }
    }
@endmod
jz
// DVI Data Island Controller with Audio
// Injects 4 DVI data island packets during horizontal blanking periods:
//   PKT0: AVI InfoFrame (video format descriptor)
//   PKT1: ACR (Audio Clock Regeneration, N=6144, CTS=37125 for 48kHz @ 37.125MHz)
//   PKT2: Audio Sample (L-PCM 2ch 16-bit, 2-3 samples per line)
//   PKT3: Audio InfoFrame (audio format descriptor)
//
// Uses shadow registers to reduce bit-extraction SELECT tables from 4 sets to 1.
// At each packet boundary, the next packet's data is copied into the shadow.
//
// Audio samples are provided externally via samp_lo/samp_hi/samp_valid ports.
//
// Data island timing within hblank (370 pixel clocks):
//   Preamble:        8 clocks  (x=1449..1456)
//   Leading guard:   2 clocks  (x=1457..1458)
//   Packet 0 (AVI):  32 clocks (x=1459..1490)
//   Packet 1 (ACR):  32 clocks (x=1491..1522)
//   Packet 2 (Audio):32 clocks (x=1523..1554)
//   Packet 3 (AIF):  32 clocks (x=1555..1586)
//   Trailing guard:  2 clocks  (x=1587..1588)
//   Control period:  51 clocks until video preamble at x=1640
@module dvi_data_island
    PORT {
        IN  [1]  clk;
        IN  [1]  rst_n;

        // Video timing inputs
        IN  [1]  hsync;
        IN  [1]  vsync;
        IN  [1]  display_enable;
        IN  [11] x_pos;

        // Audio sample inputs (from tone generator)
        IN  [32] samp_lo;       // L+R subpacket low word
        IN  [32] samp_hi;       // L+R subpacket high word
        IN  [1]  samp_valid;    // pulses high for 1 cycle when a new sample is ready

        // TERC4 data outputs (active during data island)
        OUT [4]  terc4_ch0;      // {parity, hdr_bit, vsync, hsync}
        OUT [4]  terc4_ch1;      // subpacket even bits
        OUT [4]  terc4_ch2;      // subpacket odd bits

        // Control signals
        OUT [1]  data_island_active;  // HIGH during guard bands + packet data
        OUT [1]  preamble_active;     // HIGH during data island preamble
        OUT [1]  guard_active;        // HIGH during guard bands only
    }

    CONST {
        H_ACTIVE = 1280;
        H_TOTAL  = 1650;
        V_ACTIVE = 720;

        // Data island timing
        DI_PREAMBLE_START = 1449;
        DI_GUARD_START    = 1457;
        DI_PKT0_START     = 1459;
        DI_PKT1_START     = 1491;
        DI_PKT2_START     = 1523;
        DI_PKT3_START     = 1555;
        DI_TRAIL_START    = 1587;
        DI_TRAIL_END      = 1589;

        // Shadow swap points (1 cycle before each packet start)
        DI_SHD_SWAP1      = 1490;
        DI_SHD_SWAP2      = 1522;
        DI_SHD_SWAP3      = 1554;
    }

    WIRE {
        in_hblank       [1];
        in_preamble     [1];
        in_guard_lead   [1];
        in_packet       [1];
        in_guard_trail  [1];
        in_data_island  [1];
        pkt_clock       [5];

        // Shadow bit extraction outputs
        shd_hdr_bit     [1];
        shd_sub_even    [4];
        shd_sub_odd     [4];
    }

    REGISTER {
        // Shadow registers (loaded with current packet data before each packet)
        shd_header  [32] = 32'd0;
        shd_sp0_lo  [32] = 32'd0;
        shd_sp0_hi  [32] = 32'd0;
        shd_sp1_lo  [32] = 32'd0;
        shd_sp1_hi  [32] = 32'd0;
        shd_sp2_lo  [32] = 32'd0;
        shd_sp2_hi  [32] = 32'd0;
        shd_sp3_lo  [32] = 32'd0;
        shd_sp3_hi  [32] = 32'd0;

        // Audio sample packet (PKT2) - built at H_ACTIVE from sample buffer
        p2_header   [32] = 32'd0;
        p2_sp0_lo   [32] = 32'd0;
        p2_sp0_hi   [32] = 32'd0;
        p2_sp1_lo   [32] = 32'd0;
        p2_sp1_hi   [32] = 32'd0;
        p2_sp2_lo   [32] = 32'd0;
        p2_sp2_hi   [32] = 32'd0;

        // Sample buffer (filled by samp_valid between H_ACTIVE events)
        // Up to 3 samples per line (48000/22500 = 2.133 samples/line)
        samp_buf0_lo [32] = 32'd0;
        samp_buf0_hi [32] = 32'd0;
        samp_buf1_lo [32] = 32'd0;
        samp_buf1_hi [32] = 32'd0;
        samp_buf2_lo [32] = 32'd0;
        samp_buf2_hi [32] = 32'd0;
        samp_count   [2]  = 2'd0;
    }

    ASYNCHRONOUS {
        // Blanking region detection
        in_hblank <= (display_enable == 1'b0) ? 1'b1 : 1'b0;

        // Data island sub-regions
        in_preamble <= (in_hblank == 1'b1 &&
                        x_pos >= lit(11, DI_PREAMBLE_START) && x_pos < lit(11, DI_GUARD_START)) ? 1'b1 : 1'b0;

        in_guard_lead <= (in_hblank == 1'b1 &&
                          x_pos >= lit(11, DI_GUARD_START) && x_pos < lit(11, DI_PKT0_START)) ? 1'b1 : 1'b0;

        in_packet <= (in_hblank == 1'b1 &&
                      x_pos >= lit(11, DI_PKT0_START) && x_pos < lit(11, DI_TRAIL_START)) ? 1'b1 : 1'b0;

        in_guard_trail <= (in_hblank == 1'b1 &&
                           x_pos >= lit(11, DI_TRAIL_START) && x_pos < lit(11, DI_TRAIL_END)) ? 1'b1 : 1'b0;

        in_data_island <= (in_guard_lead == 1'b1 || in_packet == 1'b1 || in_guard_trail == 1'b1) ? 1'b1 : 1'b0;

        data_island_active <= in_data_island;
        preamble_active    <= in_preamble;
        guard_active       <= (in_guard_lead == 1'b1 || in_guard_trail == 1'b1) ? 1'b1 : 1'b0;

        // Packet clock (0-31 within each packet)
        // All 4 packets start at x[4:0]=19, so this formula works for all
        pkt_clock <= x_pos[4:0] - 5'd19;

        // ---------------------------------------------------------------
        // Shadow header bit extraction (1 bit per clock, 32 bits total)
        // ---------------------------------------------------------------
        SELECT (pkt_clock) {
            CASE (5'd0)  { shd_hdr_bit <= shd_header[0]; }
            CASE (5'd1)  { shd_hdr_bit <= shd_header[1]; }
            CASE (5'd2)  { shd_hdr_bit <= shd_header[2]; }
            CASE (5'd3)  { shd_hdr_bit <= shd_header[3]; }
            CASE (5'd4)  { shd_hdr_bit <= shd_header[4]; }
            CASE (5'd5)  { shd_hdr_bit <= shd_header[5]; }
            CASE (5'd6)  { shd_hdr_bit <= shd_header[6]; }
            CASE (5'd7)  { shd_hdr_bit <= shd_header[7]; }
            CASE (5'd8)  { shd_hdr_bit <= shd_header[8]; }
            CASE (5'd9)  { shd_hdr_bit <= shd_header[9]; }
            CASE (5'd10) { shd_hdr_bit <= shd_header[10]; }
            CASE (5'd11) { shd_hdr_bit <= shd_header[11]; }
            CASE (5'd12) { shd_hdr_bit <= shd_header[12]; }
            CASE (5'd13) { shd_hdr_bit <= shd_header[13]; }
            CASE (5'd14) { shd_hdr_bit <= shd_header[14]; }
            CASE (5'd15) { shd_hdr_bit <= shd_header[15]; }
            CASE (5'd16) { shd_hdr_bit <= shd_header[16]; }
            CASE (5'd17) { shd_hdr_bit <= shd_header[17]; }
            CASE (5'd18) { shd_hdr_bit <= shd_header[18]; }
            CASE (5'd19) { shd_hdr_bit <= shd_header[19]; }
            CASE (5'd20) { shd_hdr_bit <= shd_header[20]; }
            CASE (5'd21) { shd_hdr_bit <= shd_header[21]; }
            CASE (5'd22) { shd_hdr_bit <= shd_header[22]; }
            CASE (5'd23) { shd_hdr_bit <= shd_header[23]; }
            CASE (5'd24) { shd_hdr_bit <= shd_header[24]; }
            CASE (5'd25) { shd_hdr_bit <= shd_header[25]; }
            CASE (5'd26) { shd_hdr_bit <= shd_header[26]; }
            CASE (5'd27) { shd_hdr_bit <= shd_header[27]; }
            CASE (5'd28) { shd_hdr_bit <= shd_header[28]; }
            CASE (5'd29) { shd_hdr_bit <= shd_header[29]; }
            CASE (5'd30) { shd_hdr_bit <= shd_header[30]; }
            CASE (5'd31) { shd_hdr_bit <= shd_header[31]; }
            DEFAULT      { shd_hdr_bit <= 1'b0; }
        }

        // ---------------------------------------------------------------
        // Shadow subpacket bit extraction (interleaved across 4 subpackets)
        // At clock T: ch1 = {sp3[2T], sp2[2T], sp1[2T], sp0[2T]}
        //             ch2 = {sp3[2T+1], sp2[2T+1], sp1[2T+1], sp0[2T+1]}
        // T=0..15 uses sp_lo registers, T=16..31 uses sp_hi registers
        // ---------------------------------------------------------------
        SELECT (pkt_clock) {
            CASE (5'd0)  { shd_sub_even <= { shd_sp3_lo[0],  shd_sp2_lo[0],  shd_sp1_lo[0],  shd_sp0_lo[0] };
                           shd_sub_odd  <= { shd_sp3_lo[1],  shd_sp2_lo[1],  shd_sp1_lo[1],  shd_sp0_lo[1] }; }
            CASE (5'd1)  { shd_sub_even <= { shd_sp3_lo[2],  shd_sp2_lo[2],  shd_sp1_lo[2],  shd_sp0_lo[2] };
                           shd_sub_odd  <= { shd_sp3_lo[3],  shd_sp2_lo[3],  shd_sp1_lo[3],  shd_sp0_lo[3] }; }
            CASE (5'd2)  { shd_sub_even <= { shd_sp3_lo[4],  shd_sp2_lo[4],  shd_sp1_lo[4],  shd_sp0_lo[4] };
                           shd_sub_odd  <= { shd_sp3_lo[5],  shd_sp2_lo[5],  shd_sp1_lo[5],  shd_sp0_lo[5] }; }
            CASE (5'd3)  { shd_sub_even <= { shd_sp3_lo[6],  shd_sp2_lo[6],  shd_sp1_lo[6],  shd_sp0_lo[6] };
                           shd_sub_odd  <= { shd_sp3_lo[7],  shd_sp2_lo[7],  shd_sp1_lo[7],  shd_sp0_lo[7] }; }
            CASE (5'd4)  { shd_sub_even <= { shd_sp3_lo[8],  shd_sp2_lo[8],  shd_sp1_lo[8],  shd_sp0_lo[8] };
                           shd_sub_odd  <= { shd_sp3_lo[9],  shd_sp2_lo[9],  shd_sp1_lo[9],  shd_sp0_lo[9] }; }
            CASE (5'd5)  { shd_sub_even <= { shd_sp3_lo[10], shd_sp2_lo[10], shd_sp1_lo[10], shd_sp0_lo[10] };
                           shd_sub_odd  <= { shd_sp3_lo[11], shd_sp2_lo[11], shd_sp1_lo[11], shd_sp0_lo[11] }; }
            CASE (5'd6)  { shd_sub_even <= { shd_sp3_lo[12], shd_sp2_lo[12], shd_sp1_lo[12], shd_sp0_lo[12] };
                           shd_sub_odd  <= { shd_sp3_lo[13], shd_sp2_lo[13], shd_sp1_lo[13], shd_sp0_lo[13] }; }
            CASE (5'd7)  { shd_sub_even <= { shd_sp3_lo[14], shd_sp2_lo[14], shd_sp1_lo[14], shd_sp0_lo[14] };
                           shd_sub_odd  <= { shd_sp3_lo[15], shd_sp2_lo[15], shd_sp1_lo[15], shd_sp0_lo[15] }; }
            CASE (5'd8)  { shd_sub_even <= { shd_sp3_lo[16], shd_sp2_lo[16], shd_sp1_lo[16], shd_sp0_lo[16] };
                           shd_sub_odd  <= { shd_sp3_lo[17], shd_sp2_lo[17], shd_sp1_lo[17], shd_sp0_lo[17] }; }
            CASE (5'd9)  { shd_sub_even <= { shd_sp3_lo[18], shd_sp2_lo[18], shd_sp1_lo[18], shd_sp0_lo[18] };
                           shd_sub_odd  <= { shd_sp3_lo[19], shd_sp2_lo[19], shd_sp1_lo[19], shd_sp0_lo[19] }; }
            CASE (5'd10) { shd_sub_even <= { shd_sp3_lo[20], shd_sp2_lo[20], shd_sp1_lo[20], shd_sp0_lo[20] };
                           shd_sub_odd  <= { shd_sp3_lo[21], shd_sp2_lo[21], shd_sp1_lo[21], shd_sp0_lo[21] }; }
            CASE (5'd11) { shd_sub_even <= { shd_sp3_lo[22], shd_sp2_lo[22], shd_sp1_lo[22], shd_sp0_lo[22] };
                           shd_sub_odd  <= { shd_sp3_lo[23], shd_sp2_lo[23], shd_sp1_lo[23], shd_sp0_lo[23] }; }
            CASE (5'd12) { shd_sub_even <= { shd_sp3_lo[24], shd_sp2_lo[24], shd_sp1_lo[24], shd_sp0_lo[24] };
                           shd_sub_odd  <= { shd_sp3_lo[25], shd_sp2_lo[25], shd_sp1_lo[25], shd_sp0_lo[25] }; }
            CASE (5'd13) { shd_sub_even <= { shd_sp3_lo[26], shd_sp2_lo[26], shd_sp1_lo[26], shd_sp0_lo[26] };
                           shd_sub_odd  <= { shd_sp3_lo[27], shd_sp2_lo[27], shd_sp1_lo[27], shd_sp0_lo[27] }; }
            CASE (5'd14) { shd_sub_even <= { shd_sp3_lo[28], shd_sp2_lo[28], shd_sp1_lo[28], shd_sp0_lo[28] };
                           shd_sub_odd  <= { shd_sp3_lo[29], shd_sp2_lo[29], shd_sp1_lo[29], shd_sp0_lo[29] }; }
            CASE (5'd15) { shd_sub_even <= { shd_sp3_lo[30], shd_sp2_lo[30], shd_sp1_lo[30], shd_sp0_lo[30] };
                           shd_sub_odd  <= { shd_sp3_lo[31], shd_sp2_lo[31], shd_sp1_lo[31], shd_sp0_lo[31] }; }
            CASE (5'd16) { shd_sub_even <= { shd_sp3_hi[0],  shd_sp2_hi[0],  shd_sp1_hi[0],  shd_sp0_hi[0] };
                           shd_sub_odd  <= { shd_sp3_hi[1],  shd_sp2_hi[1],  shd_sp1_hi[1],  shd_sp0_hi[1] }; }
            CASE (5'd17) { shd_sub_even <= { shd_sp3_hi[2],  shd_sp2_hi[2],  shd_sp1_hi[2],  shd_sp0_hi[2] };
                           shd_sub_odd  <= { shd_sp3_hi[3],  shd_sp2_hi[3],  shd_sp1_hi[3],  shd_sp0_hi[3] }; }
            CASE (5'd18) { shd_sub_even <= { shd_sp3_hi[4],  shd_sp2_hi[4],  shd_sp1_hi[4],  shd_sp0_hi[4] };
                           shd_sub_odd  <= { shd_sp3_hi[5],  shd_sp2_hi[5],  shd_sp1_hi[5],  shd_sp0_hi[5] }; }
            CASE (5'd19) { shd_sub_even <= { shd_sp3_hi[6],  shd_sp2_hi[6],  shd_sp1_hi[6],  shd_sp0_hi[6] };
                           shd_sub_odd  <= { shd_sp3_hi[7],  shd_sp2_hi[7],  shd_sp1_hi[7],  shd_sp0_hi[7] }; }
            CASE (5'd20) { shd_sub_even <= { shd_sp3_hi[8],  shd_sp2_hi[8],  shd_sp1_hi[8],  shd_sp0_hi[8] };
                           shd_sub_odd  <= { shd_sp3_hi[9],  shd_sp2_hi[9],  shd_sp1_hi[9],  shd_sp0_hi[9] }; }
            CASE (5'd21) { shd_sub_even <= { shd_sp3_hi[10], shd_sp2_hi[10], shd_sp1_hi[10], shd_sp0_hi[10] };
                           shd_sub_odd  <= { shd_sp3_hi[11], shd_sp2_hi[11], shd_sp1_hi[11], shd_sp0_hi[11] }; }
            CASE (5'd22) { shd_sub_even <= { shd_sp3_hi[12], shd_sp2_hi[12], shd_sp1_hi[12], shd_sp0_hi[12] };
                           shd_sub_odd  <= { shd_sp3_hi[13], shd_sp2_hi[13], shd_sp1_hi[13], shd_sp0_hi[13] }; }
            CASE (5'd23) { shd_sub_even <= { shd_sp3_hi[14], shd_sp2_hi[14], shd_sp1_hi[14], shd_sp0_hi[14] };
                           shd_sub_odd  <= { shd_sp3_hi[15], shd_sp2_hi[15], shd_sp1_hi[15], shd_sp0_hi[15] }; }
            CASE (5'd24) { shd_sub_even <= { shd_sp3_hi[16], shd_sp2_hi[16], shd_sp1_hi[16], shd_sp0_hi[16] };
                           shd_sub_odd  <= { shd_sp3_hi[17], shd_sp2_hi[17], shd_sp1_hi[17], shd_sp0_hi[17] }; }
            CASE (5'd25) { shd_sub_even <= { shd_sp3_hi[18], shd_sp2_hi[18], shd_sp1_hi[18], shd_sp0_hi[18] };
                           shd_sub_odd  <= { shd_sp3_hi[19], shd_sp2_hi[19], shd_sp1_hi[19], shd_sp0_hi[19] }; }
            CASE (5'd26) { shd_sub_even <= { shd_sp3_hi[20], shd_sp2_hi[20], shd_sp1_hi[20], shd_sp0_hi[20] };
                           shd_sub_odd  <= { shd_sp3_hi[21], shd_sp2_hi[21], shd_sp1_hi[21], shd_sp0_hi[21] }; }
            CASE (5'd27) { shd_sub_even <= { shd_sp3_hi[22], shd_sp2_hi[22], shd_sp1_hi[22], shd_sp0_hi[22] };
                           shd_sub_odd  <= { shd_sp3_hi[23], shd_sp2_hi[23], shd_sp1_hi[23], shd_sp0_hi[23] }; }
            CASE (5'd28) { shd_sub_even <= { shd_sp3_hi[24], shd_sp2_hi[24], shd_sp1_hi[24], shd_sp0_hi[24] };
                           shd_sub_odd  <= { shd_sp3_hi[25], shd_sp2_hi[25], shd_sp1_hi[25], shd_sp0_hi[25] }; }
            CASE (5'd29) { shd_sub_even <= { shd_sp3_hi[26], shd_sp2_hi[26], shd_sp1_hi[26], shd_sp0_hi[26] };
                           shd_sub_odd  <= { shd_sp3_hi[27], shd_sp2_hi[27], shd_sp1_hi[27], shd_sp0_hi[27] }; }
            CASE (5'd30) { shd_sub_even <= { shd_sp3_hi[28], shd_sp2_hi[28], shd_sp1_hi[28], shd_sp0_hi[28] };
                           shd_sub_odd  <= { shd_sp3_hi[29], shd_sp2_hi[29], shd_sp1_hi[29], shd_sp0_hi[29] }; }
            CASE (5'd31) { shd_sub_even <= { shd_sp3_hi[30], shd_sp2_hi[30], shd_sp1_hi[30], shd_sp0_hi[30] };
                           shd_sub_odd  <= { shd_sp3_hi[31], shd_sp2_hi[31], shd_sp1_hi[31], shd_sp0_hi[31] }; }
            DEFAULT      { shd_sub_even <= 4'b0000; shd_sub_odd <= 4'b0000; }
        }

        // TERC4 channel outputs
        IF (in_guard_lead == 1'b1 || in_guard_trail == 1'b1) {
            terc4_ch0 <= { 2'b11, vsync, hsync };
            terc4_ch1 <= 4'b0000;
            terc4_ch2 <= 4'b0000;
        } ELIF (in_packet == 1'b1) {
            terc4_ch0 <= { 1'b1, shd_hdr_bit, vsync, hsync };
            terc4_ch1 <= shd_sub_even;
            terc4_ch2 <= shd_sub_odd;
        } ELSE {
            terc4_ch0 <= { 2'b11, vsync, hsync };
            terc4_ch1 <= 4'b0000;
            terc4_ch2 <= 4'b0000;
        }
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        // ---- Packet loading and shadow management ----
        // At H_ACTIVE: build audio sample packet from buffer, load shadow with PKT0
        // At swap points: load shadow with next packet's data
        // Sample buffering runs in all non-H_ACTIVE branches
        IF (x_pos == lit(11, H_ACTIVE)) {
            // Build audio sample packet (PKT2) from accumulated samples
            IF (samp_count == 2'd3) {
                // 3 samples collected
                p2_header <= 32'h4D000702;
                p2_sp0_lo <= samp_buf0_lo;
                p2_sp0_hi <= samp_buf0_hi;
                p2_sp1_lo <= samp_buf1_lo;
                p2_sp1_hi <= samp_buf1_hi;
                p2_sp2_lo <= samp_buf2_lo;
                p2_sp2_hi <= samp_buf2_hi;
            } ELIF (samp_count == 2'd2) {
                // 2 samples collected
                p2_header <= 32'h80000302;
                p2_sp0_lo <= samp_buf0_lo;
                p2_sp0_hi <= samp_buf0_hi;
                p2_sp1_lo <= samp_buf1_lo;
                p2_sp1_hi <= samp_buf1_hi;
                p2_sp2_lo <= 32'd0;
                p2_sp2_hi <= 32'd0;
            } ELIF (samp_count == 2'd1) {
                // 1 sample collected (first line after reset)
                p2_header <= 32'h65000102;
                p2_sp0_lo <= samp_buf0_lo;
                p2_sp0_hi <= samp_buf0_hi;
                p2_sp1_lo <= 32'd0;
                p2_sp1_hi <= 32'd0;
                p2_sp2_lo <= 32'd0;
                p2_sp2_hi <= 32'd0;
            } ELSE {
                // No samples - send silence
                p2_header <= 32'h65000102;
                p2_sp0_lo <= 32'd0;
                p2_sp0_hi <= 32'd0;
                p2_sp1_lo <= 32'd0;
                p2_sp1_hi <= 32'd0;
                p2_sp2_lo <= 32'd0;
                p2_sp2_hi <= 32'd0;
            }

            // Reset sample buffer for next line, capturing if samp_valid fires now
            IF (samp_valid == 1'b1) {
                samp_buf0_lo <= samp_lo;
                samp_buf0_hi <= samp_hi;
                samp_count   <= 2'd1;
            } ELSE {
                samp_count <= 2'd0;
            }

            // Load shadow with PKT0: AVI InfoFrame
            // Header: {ECC=0xE4, Len=0x0D, Ver=0x02, Type=0x82}
            shd_header <= 32'hE40D0282;
            // SP0: {PB3=0, PB2=0, PB1=0, PB0=checksum=0x6F}
            shd_sp0_lo <= 32'h0000006F;
            shd_sp0_hi <= 32'h5F000000;
            shd_sp1_lo <= 32'd0;
            shd_sp1_hi <= 32'd0;
            shd_sp2_lo <= 32'd0;
            shd_sp2_hi <= 32'd0;
            shd_sp3_lo <= 32'd0;
            shd_sp3_hi <= 32'd0;

        } ELIF (x_pos == lit(11, DI_SHD_SWAP1)) {
            // Shadow <- PKT1: ACR (N=6144, CTS=37125=0x009105)
            // Header: {ECC=0x4A, 0x00, 0x00, Type=0x01}
            shd_header <= 32'h4A000001;
            // SP0: {CTS[7:0]=0x05, CTS[15:8]=0x91, CTS[19:16]=0x00, 0x00}
            shd_sp0_lo <= 32'h05910000;
            // SP0 hi: {ECC=0x16, N[7:0]=0x00, N[15:8]=0x18, N[19:16]=0x00}
            shd_sp0_hi <= 32'h16001800;
            shd_sp1_lo <= 32'd0;
            shd_sp1_hi <= 32'd0;
            shd_sp2_lo <= 32'd0;
            shd_sp2_hi <= 32'd0;
            shd_sp3_lo <= 32'd0;
            shd_sp3_hi <= 32'd0;

            // Sample buffering (samp_valid may fire this cycle)
            IF (samp_valid == 1'b1) {
                IF (samp_count == 2'd0) {
                    samp_buf0_lo <= samp_lo;
                    samp_buf0_hi <= samp_hi;
                    samp_count   <= 2'd1;
                } ELIF (samp_count == 2'd1) {
                    samp_buf1_lo <= samp_lo;
                    samp_buf1_hi <= samp_hi;
                    samp_count   <= 2'd2;
                } ELIF (samp_count == 2'd2) {
                    samp_buf2_lo <= samp_lo;
                    samp_buf2_hi <= samp_hi;
                    samp_count   <= 2'd3;
                }
            }

        } ELIF (x_pos == lit(11, DI_SHD_SWAP2)) {
            // Shadow <- PKT2: Audio Sample (from pre-built registers)
            shd_header <= p2_header;
            shd_sp0_lo <= p2_sp0_lo;
            shd_sp0_hi <= p2_sp0_hi;
            shd_sp1_lo <= p2_sp1_lo;
            shd_sp1_hi <= p2_sp1_hi;
            shd_sp2_lo <= p2_sp2_lo;
            shd_sp2_hi <= p2_sp2_hi;
            shd_sp3_lo <= 32'd0;
            shd_sp3_hi <= 32'd0;

            // Sample buffering
            IF (samp_valid == 1'b1) {
                IF (samp_count == 2'd0) {
                    samp_buf0_lo <= samp_lo;
                    samp_buf0_hi <= samp_hi;
                    samp_count   <= 2'd1;
                } ELIF (samp_count == 2'd1) {
                    samp_buf1_lo <= samp_lo;
                    samp_buf1_hi <= samp_hi;
                    samp_count   <= 2'd2;
                }
            }

        } ELIF (x_pos == lit(11, DI_SHD_SWAP3)) {
            // Shadow <- PKT3: Audio InfoFrame (2ch L-PCM 48kHz 16-bit)
            // Header: {ECC=0x4A, Len=0x0A, Ver=0x01, Type=0x84}
            shd_header <= 32'h4A0A0184;
            // SP0: {PB3=0, PB2=0x0D(48kHz/16bit), PB1=0x11(PCM/2ch), PB0=chksum=0x53}
            shd_sp0_lo <= 32'h000D1153;
            shd_sp0_hi <= 32'hB9000000;
            shd_sp1_lo <= 32'd0;
            shd_sp1_hi <= 32'd0;
            shd_sp2_lo <= 32'd0;
            shd_sp2_hi <= 32'd0;
            shd_sp3_lo <= 32'd0;
            shd_sp3_hi <= 32'd0;

            // Sample buffering
            IF (samp_valid == 1'b1) {
                IF (samp_count == 2'd0) {
                    samp_buf0_lo <= samp_lo;
                    samp_buf0_hi <= samp_hi;
                    samp_count   <= 2'd1;
                } ELIF (samp_count == 2'd1) {
                    samp_buf1_lo <= samp_lo;
                    samp_buf1_hi <= samp_hi;
                    samp_count   <= 2'd2;
                }
            }

        } ELSE {
            // Default cycle: sample buffering only
            IF (samp_valid == 1'b1) {
                IF (samp_count == 2'd0) {
                    samp_buf0_lo <= samp_lo;
                    samp_buf0_hi <= samp_hi;
                    samp_count   <= 2'd1;
                } ELIF (samp_count == 2'd1) {
                    samp_buf1_lo <= samp_lo;
                    samp_buf1_hi <= samp_hi;
                    samp_count   <= 2'd2;
                } ELIF (samp_count == 2'd2) {
                    samp_buf2_lo <= samp_lo;
                    samp_buf2_hi <= samp_hi;
                    samp_count   <= 2'd3;
                }
            }
        }
    }
@endmod
jz
// TERC4 (Transition-minimized Error Reduction Coding, 4-bit)
// DVI/HDMI 1.4 spec encoding for data island periods.
// 4-bit input -> 10-bit TERC4 output, purely combinational.
@module terc4_encoder
    PORT {
        IN  [4]  data_in;
        OUT [10] terc4_out;
    }

    WIRE {
        result [10];
    }

    ASYNCHRONOUS {
        SELECT (data_in) {
            CASE (4'b0000) { result <= 10'b1010011100; }
            CASE (4'b0001) { result <= 10'b1001100011; }
            CASE (4'b0010) { result <= 10'b1011100100; }
            CASE (4'b0011) { result <= 10'b1011100010; }
            CASE (4'b0100) { result <= 10'b0101110001; }
            CASE (4'b0101) { result <= 10'b0100011110; }
            CASE (4'b0110) { result <= 10'b0110001110; }
            CASE (4'b0111) { result <= 10'b0100111100; }
            CASE (4'b1000) { result <= 10'b1011001100; }
            CASE (4'b1001) { result <= 10'b0100111001; }
            CASE (4'b1010) { result <= 10'b0110011100; }
            CASE (4'b1011) { result <= 10'b1011000110; }
            CASE (4'b1100) { result <= 10'b1010001110; }
            CASE (4'b1101) { result <= 10'b1001110001; }
            CASE (4'b1110) { result <= 10'b0101100011; }
            CASE (4'b1111) { result <= 10'b1011000011; }
            DEFAULT        { result <= 10'b1010011100; }
        }
        terc4_out <= result;
    }
@endmod
jz
// Tone Generator
// Reads melody data from BRAM, sequences notes, runs a Bresenham 48kHz
// sample clock, and outputs raw signed 16-bit PCM samples.
// Square wave tone with per-note articulation gap and volume.
// Each instance reads its own track binary file (up to 500 notes).
@module tone_gen
    CONST {
        DATA_FILE = "../out/track0.bin";
    }

    PORT {
        IN  [1]  clk;
        IN  [1]  rst_n;
        IN  [1]  next_song;     // single-cycle pulse to switch to next song
        OUT [16] sample;        // raw signed 16-bit PCM
        OUT [1]  samp_valid;    // pulses high for 1 cycle when a new sample is ready
        OUT [16] half_period_out; // current note half-period (frequency indicator)
        OUT [8]  volume_out;      // current note volume (amplitude envelope)
        OUT [1]  in_gap_out;      // articulation gap flag
    }

    CONST {
        // Bresenham audio sample rate: 48000/37125000 = 16/12375
        BRES_STEP         = 16;
        BRES_THRESH       = 12375;
        BRES_FIRE_MIN     = 12359;
    }

    WIRE {
        // Bresenham pre-computation
        bres_fire       [1];
        bres_next       [14];

        // Melody/tone wires
        mel_addr         [11];
        mel_half_period  [16];
        mel_duration     [24];
        mel_gap          [16];
        mel_volume       [8];
        at_song_end      [1];
        cur_half_period  [16];
        cur_note_dur     [24];
        cur_gap          [16];
        cur_volume       [8];
        in_gap           [1];

        // Current audio sample value
        cur_sample       [16];

        // 2-tap moving average filter
        filt_sum         [17];          // sadd sign-extended sum
        filt_out         [16];          // averaged output
    }

    REGISTER {
        // Bresenham audio sample clock accumulator
        bres_acc     [14] = 14'd0;

        // Melody sequencer and tone generator
        tone_phase    [16] = 16'd0;
        tone_polarity [1]  = 1'b0;
        note_idx      [10] = 10'd0;
        song_base     [11] = 11'd0;   // 0=song 1, 500=song 2, 1000=song 3
        note_dur_cnt  [24] = 24'd0;

        // Sample valid pulse
        samp_valid_r  [1]  = 1'b0;

        // Previous sample for 2-tap moving average filter
        prev_sample   [16] = 16'd0;
    }

    @new mel0 melodies {
        OVERRIDE {
            DATA_FILE = DATA_FILE;
        }
        IN  [1]  clk          = clk;
        IN  [1]  rst_n        = rst_n;
        IN  [11] addr         = mel_addr;
        OUT [16] half_period  = mel_half_period;
        OUT [24] duration     = mel_duration;
        OUT [16] gap          = mel_gap;
        OUT [8]  volume       = mel_volume;
    }

    ASYNCHRONOUS {
        // ---- Bresenham pre-computation ----
        bres_fire <= (bres_acc >= lit(14, BRES_FIRE_MIN)) ? 1'b1 : 1'b0;
        IF (bres_acc >= lit(14, BRES_FIRE_MIN)) {
            bres_next <= bres_acc - lit(14, BRES_FIRE_MIN);
        } ELSE {
            bres_next <= bres_acc + lit(14, BRES_STEP);
        }

        // ---- Melody BRAM address ----
        mel_addr <= song_base + { 1'b0, note_idx };

        // Drive current note parameters from BRAM output
        cur_half_period <= mel_half_period;
        cur_note_dur    <= mel_duration;
        cur_gap         <= mel_gap;
        cur_volume      <= mel_volume;

        // Sentinel detection: half_period = 0xFFFF marks end of song
        at_song_end <= (mel_half_period == 16'hFFFF) ? 1'b1 : 1'b0;

        // Gap: silence during last N samples of each note for articulation
        in_gap <= (note_dur_cnt >= cur_note_dur - { 8'h00, cur_gap }) ? 1'b1 : 1'b0;

        // ---- Current audio sample value (raw signed 16-bit PCM) ----
        // Volume is unsigned 8-bit, scaled up 8x (shift left 3) so the
        // 4-channel mix uses ~15% of the 16-bit range (max sum ~4800).
        IF (in_gap == 1'b1) {
            cur_sample <= 16'h0000;
        } ELIF (tone_polarity == 1'b0) {
            cur_sample <= { 5'h00, cur_volume, 3'h0 };
        } ELSE {
            cur_sample <= 16'h0000 - { 5'h00, cur_volume, 3'h0 };
        }

        // 2-tap moving average anti-alias filter: (cur + prev) / 2
        filt_sum <= sadd(cur_sample, prev_sample);
        filt_out <= filt_sum[16:1];

        // Drive output ports
        sample         <= filt_out;
        samp_valid     <= samp_valid_r;
        half_period_out <= cur_half_period;
        volume_out     <= cur_volume;
        in_gap_out     <= in_gap;
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        // ---- Bresenham accumulator (runs every cycle) ----
        bres_acc <= bres_next;

        // ---- Sample valid pulse: high for 1 cycle when Bresenham fires ----
        samp_valid_r <= bres_fire;

        // ---- Capture previous sample for moving average filter ----
        IF (bres_fire == 1'b1) {
            prev_sample <= cur_sample;
        }

        // ---- Song switch: cycle song_base on next_song pulse ----
        IF (next_song == 1'b1) {
            IF (song_base == 11'd0) {
                song_base <= 11'd500;
            } ELIF (song_base == 11'd500) {
                song_base <= 11'd1000;
            } ELSE {
                song_base <= 11'd0;
            }
            note_idx      <= 10'd0;
            note_dur_cnt  <= 24'd0;
            tone_phase    <= 16'd0;
            tone_polarity <= 1'b0;
        } ELSE {
            // ---- Audio state advance ----
            // Song-end sentinel: wrap back to start immediately
            IF (at_song_end == 1'b1) {
                note_idx      <= 10'd0;
                note_dur_cnt  <= 24'd0;
                tone_phase    <= 16'd0;
                tone_polarity <= 1'b0;
            } ELIF (bres_fire == 1'b1) {
                // Note boundary: advance to next note and reset tone
                IF (note_dur_cnt + 24'd1 >= cur_note_dur) {
                    note_dur_cnt  <= 24'd0;
                    tone_phase    <= 16'd0;
                    tone_polarity <= 1'b0;
                    note_idx      <= note_idx + 10'd1;
                } ELSE {
                    note_dur_cnt <= note_dur_cnt + 24'd1;
                    // Advance tone: toggle polarity at half-period boundary
                    IF (tone_phase + 16'd1 >= cur_half_period) {
                        tone_phase    <= 16'd0;
                        tone_polarity <= ~tone_polarity;
                    } ELSE {
                        tone_phase <= tone_phase + 16'd1;
                    }
                }
            }
        }
    }
@endmod
jz
// Melody BRAM wrapper
// Stores 64-bit note entries as pairs of 32-bit words in BRAM.
// Entry format: {half_period[63:48], duration[47:24], gap[23:8], volume[7:0]}
//   Even address (note*2):   {half_period[15:0], duration[23:8]}
//   Odd  address (note*2+1): {duration[7:0], gap[15:0], volume[7:0]}
// Sentinel: half_period = 0xFFFF marks end of song
// 500 notes per track, one binary file per track.
//
// Latency: 3 cycles (phase toggle reads 2 BRAM words). Negligible for audio.
@module melodies
    CONST {
        DATA_FILE = "../out/track0.bin";
    }

    PORT {
        IN  [1]  clk;
        IN  [1]  rst_n;
        IN  [11] addr;
        OUT [16] half_period;
        OUT [24] duration;
        OUT [16] gap;
        OUT [8]  volume;
    }

    MEM(TYPE=BLOCK) {
        mel [32] [3072] = @file(DATA_FILE) {
            OUT read SYNC;
        };
    }

    WIRE {
        read_addr [12];
    }

    REGISTER {
        phase    [1]  = 1'b0;
        word_hi  [32] = 32'd0;
        word_lo  [32] = 32'd0;
    }

    ASYNCHRONOUS {
        // Note address doubled, phase selects hi/lo word
        read_addr <= { addr, phase };

        // Extract fields from captured word pair
        // Hi word: {half_period[15:0], duration[23:8]}
        // Lo word: {duration[7:0], gap[15:0], volume[7:0]}
        half_period <= word_hi[31:16];
        duration    <= { word_hi[15:0], word_lo[31:24] };
        gap         <= word_lo[23:8];
        volume      <= word_lo[7:0];
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        mel.read.addr <= read_addr;
        phase <= ~phase;

        // Capture BRAM data: hi word arrives when phase=1, lo word when phase=0
        IF (phase == 1'b1) {
            word_hi <= mel.read.data;
        } ELSE {
            word_lo <= mel.read.data;
        }
    }
@endmod
jz
// 4-Channel Audio Mixer + DVI Subpacket Formatter
// Sums 4 signed 16-bit PCM channels and formats as DVI audio subpacket words.
// Includes BCH(64,56) ECC computation (polynomial 0x83, LSB-first).
// Purely combinational (ASYNC only).
@module mixer
    PORT {
        IN  [16] s0;            // channel 0 PCM sample
        IN  [16] s1;            // channel 1 PCM sample
        IN  [16] s2;            // channel 2 PCM sample
        IN  [16] s3;            // channel 3 PCM sample
        IN  [1]  samp_valid;    // sample valid pulse
        OUT [32] samp_lo;       // DVI subpacket low word
        OUT [32] samp_hi;       // DVI subpacket high word
        OUT [1]  out_valid;     // pass-through valid
    }

    WIRE {
        sum01    [17];          // sadd(s0, s1) — 17-bit signed
        sum23    [17];          // sadd(s2, s3) — 17-bit signed
        sum_all  [18];          // sadd(sum01, sum23) — 18-bit signed
        mix      [16];          // final 16-bit signed PCM
        samp_24  [24];          // left-justified to 24 bits

        // Parity computation (XOR of all 24 bits, folded in stages)
        p8       [8];           // byte0 ^ byte1 ^ byte2
        p4       [4];           // fold 8 to 4
        p2       [2];           // fold 4 to 2
        parity   [1];           // final even parity bit
        sb6      [8];           // status byte 6

        // Subpacket data (internal wires, before port assignment)
        lo       [32];          // samp_lo value
        hi_data  [24];          // upper 24 bits: {sb6, R[23:16], R[15:8]}

        // BCH ECC
        ecc      [8];           // BCH(64,56) ECC result
    }

    ASYNCHRONOUS {
        // Sum 4 channels using sadd for proper sign-extended widening
        sum01   <= sadd(s0, s1);
        sum23   <= sadd(s2, s3);
        sum_all <= sadd(sum01, sum23);
        mix     <= sum_all[15:0];

        // Left-justify 16-bit sample to 24 bits
        samp_24 <= { mix, 8'h00 };

        // Even parity: XOR all 24 bits by folding bytes then nibbles
        p8     <= samp_24[7:0] ^ samp_24[15:8] ^ samp_24[23:16];
        p4     <= p8[3:0] ^ p8[7:4];
        p2     <= p4[1:0] ^ p4[3:2];
        parity <= p2[0] ^ p2[1];

        // Status byte 6: {P_L, 000, P_R, 000} -- parity for both L and R
        sb6 <= { parity, 3'b000, parity, 3'b000 };

        // DVI subpacket format (L=R mono):
        //   lo = { R[7:0], L[23:16], L[15:8], L[7:0] }
        lo <= { samp_24[7:0], samp_24[23:16], samp_24[15:8], samp_24[7:0] };

        // Upper 24 data bits for ECC: {sb6, R[23:16], R[15:8]}
        hi_data <= { sb6, samp_24[23:16], samp_24[15:8] };

        // BCH(64,56) ECC: polynomial 0x83, LSB-first, right-shift LFSR
        // data[31:0] = lo, data[55:32] = hi_data
        ecc[0] <= lo[0] ^ lo[1] ^ lo[3] ^ lo[4] ^ lo[5] ^ lo[6] ^ lo[11] ^ lo[12] ^ lo[14] ^ lo[17] ^ lo[21] ^ lo[22] ^ lo[23] ^ lo[24] ^ lo[25] ^ lo[27] ^ lo[29] ^ lo[30] ^ lo[31] ^ hi_data[2] ^ hi_data[5] ^ hi_data[7] ^ hi_data[8] ^ hi_data[9] ^ hi_data[10] ^ hi_data[11] ^ hi_data[12] ^ hi_data[15] ^ hi_data[16] ^ hi_data[17] ^ hi_data[18] ^ hi_data[20] ^ hi_data[21] ^ hi_data[23];
        ecc[1] <= lo[0] ^ lo[2] ^ lo[3] ^ lo[7] ^ lo[11] ^ lo[13] ^ lo[14] ^ lo[15] ^ lo[17] ^ lo[18] ^ lo[21] ^ lo[26] ^ lo[27] ^ lo[28] ^ lo[29] ^ hi_data[0] ^ hi_data[2] ^ hi_data[3] ^ hi_data[5] ^ hi_data[6] ^ hi_data[7] ^ hi_data[13] ^ hi_data[15] ^ hi_data[19] ^ hi_data[20] ^ hi_data[22] ^ hi_data[23];
        ecc[2] <= lo[0] ^ lo[5] ^ lo[6] ^ lo[8] ^ lo[11] ^ lo[15] ^ lo[16] ^ lo[17] ^ lo[18] ^ lo[19] ^ lo[21] ^ lo[23] ^ lo[24] ^ lo[25] ^ lo[28] ^ lo[31] ^ hi_data[1] ^ hi_data[2] ^ hi_data[3] ^ hi_data[4] ^ hi_data[5] ^ hi_data[6] ^ hi_data[9] ^ hi_data[10] ^ hi_data[11] ^ hi_data[12] ^ hi_data[14] ^ hi_data[15] ^ hi_data[17] ^ hi_data[18];
        ecc[3] <= lo[0] ^ lo[1] ^ lo[6] ^ lo[7] ^ lo[9] ^ lo[12] ^ lo[16] ^ lo[17] ^ lo[18] ^ lo[19] ^ lo[20] ^ lo[22] ^ lo[24] ^ lo[25] ^ lo[26] ^ lo[29] ^ hi_data[0] ^ hi_data[2] ^ hi_data[3] ^ hi_data[4] ^ hi_data[5] ^ hi_data[6] ^ hi_data[7] ^ hi_data[10] ^ hi_data[11] ^ hi_data[12] ^ hi_data[13] ^ hi_data[15] ^ hi_data[16] ^ hi_data[18] ^ hi_data[19];
        ecc[4] <= lo[0] ^ lo[1] ^ lo[2] ^ lo[7] ^ lo[8] ^ lo[10] ^ lo[13] ^ lo[17] ^ lo[18] ^ lo[19] ^ lo[20] ^ lo[21] ^ lo[23] ^ lo[25] ^ lo[26] ^ lo[27] ^ lo[30] ^ hi_data[1] ^ hi_data[3] ^ hi_data[4] ^ hi_data[5] ^ hi_data[6] ^ hi_data[7] ^ hi_data[8] ^ hi_data[11] ^ hi_data[12] ^ hi_data[13] ^ hi_data[14] ^ hi_data[16] ^ hi_data[17] ^ hi_data[19] ^ hi_data[20];
        ecc[5] <= lo[0] ^ lo[1] ^ lo[2] ^ lo[3] ^ lo[8] ^ lo[9] ^ lo[11] ^ lo[14] ^ lo[18] ^ lo[19] ^ lo[20] ^ lo[21] ^ lo[22] ^ lo[24] ^ lo[26] ^ lo[27] ^ lo[28] ^ lo[31] ^ hi_data[2] ^ hi_data[4] ^ hi_data[5] ^ hi_data[6] ^ hi_data[7] ^ hi_data[8] ^ hi_data[9] ^ hi_data[12] ^ hi_data[13] ^ hi_data[14] ^ hi_data[15] ^ hi_data[17] ^ hi_data[18] ^ hi_data[20] ^ hi_data[21];
        ecc[6] <= lo[1] ^ lo[2] ^ lo[3] ^ lo[4] ^ lo[9] ^ lo[10] ^ lo[12] ^ lo[15] ^ lo[19] ^ lo[20] ^ lo[21] ^ lo[22] ^ lo[23] ^ lo[25] ^ lo[27] ^ lo[28] ^ lo[29] ^ hi_data[0] ^ hi_data[3] ^ hi_data[5] ^ hi_data[6] ^ hi_data[7] ^ hi_data[8] ^ hi_data[9] ^ hi_data[10] ^ hi_data[13] ^ hi_data[14] ^ hi_data[15] ^ hi_data[16] ^ hi_data[18] ^ hi_data[19] ^ hi_data[21] ^ hi_data[22];
        ecc[7] <= lo[0] ^ lo[2] ^ lo[3] ^ lo[4] ^ lo[5] ^ lo[10] ^ lo[11] ^ lo[13] ^ lo[16] ^ lo[20] ^ lo[21] ^ lo[22] ^ lo[23] ^ lo[24] ^ lo[26] ^ lo[28] ^ lo[29] ^ lo[30] ^ hi_data[1] ^ hi_data[4] ^ hi_data[6] ^ hi_data[7] ^ hi_data[8] ^ hi_data[9] ^ hi_data[10] ^ hi_data[11] ^ hi_data[14] ^ hi_data[15] ^ hi_data[16] ^ hi_data[17] ^ hi_data[19] ^ hi_data[20] ^ hi_data[22] ^ hi_data[23];

        // Drive output ports
        samp_lo  <= lo;
        samp_hi  <= { ecc, hi_data };
        out_valid <= samp_valid;
    }
@endmod
jz
// 80-Bar Spectrum Analyzer
// Maps 4 audio channels' frequencies to 80 log-spaced bins (65 Hz - 5 kHz),
// adds simulated odd harmonics (3rd, 5th) with spectral spread for visual fullness.
// Smooths amplitudes per frame, stores in DISTRIBUTED RAM.
// Exposes combinational read port for the display module.
@module spectrum_analyzer
    PORT {
        IN  [1]  clk;
        IN  [1]  rst_n;
        IN  [16] ch0_half_period;  IN  [8] ch0_volume;  IN  [1] ch0_in_gap;
        IN  [16] ch1_half_period;  IN  [8] ch1_volume;  IN  [1] ch1_in_gap;
        IN  [16] ch2_half_period;  IN  [8] ch2_volume;  IN  [1] ch2_in_gap;
        IN  [16] ch3_half_period;  IN  [8] ch3_volume;  IN  [1] ch3_in_gap;
        IN  [1]  frame_pulse;
        IN  [7]  rd_bar;
        OUT [16] rd_amp;
    }

    WIRE {
        // Per-channel bar indices (combinational from half_period)
        ch0_fund [7];  ch0_h3 [7];  ch0_h5 [7];
        ch1_fund [7];  ch1_h3 [7];  ch1_h5 [7];
        ch2_fund [7];  ch2_h3 [7];  ch2_h5 [7];
        ch3_fund [7];  ch3_h3 [7];  ch3_h5 [7];

        // Per-channel effective volume (0 during gap)
        ch0_vol_eff [8];  ch1_vol_eff [8];
        ch2_vol_eff [8];  ch3_vol_eff [8];

        // Signed distance from sweep_idx to each source bar
        d0f [8];  d0h3 [8];  d0h5 [8];
        d1f [8];  d1h3 [8];  d1h5 [8];
        d2f [8];  d2h3 [8];  d2h5 [8];
        d3f [8];  d3h3 [8];  d3h5 [8];

        // Absolute distances
        a0f [7];  a0h3 [7];  a0h5 [7];
        a1f [7];  a1h3 [7];  a1h5 [7];
        a2f [7];  a2h3 [7];  a2h5 [7];
        a3f [7];  a3h3 [7];  a3h5 [7];

        // 12 separate contribution values (no accumulation)
        c0f [8];  c0h3 [8];  c0h5 [8];
        c1f [8];  c1h3 [8];  c1h5 [8];
        c2f [8];  c2h3 [8];  c2h5 [8];
        c3f [8];  c3h3 [8];  c3h5 [8];

        // Sum tree
        sum_01f  [9];   sum_23f  [9];
        sum_01h3 [9];   sum_23h3 [9];
        sum_01h5 [9];   sum_23h5 [9];
        sum_fund [10];  sum_h3   [10];  sum_h5 [10];
        target_acc [11];
        target_amp [8];

        // Smoothing
        smooth_cur    [8];
        smooth_delta  [9];
        smooth_step   [9];
        smooth_next   [9];
        smooth_result [8];

        // Peak (from sweep_ram: [15:12]=peak_top4, [11:8]=timer)
        peak_approx   [8];    // reconstructed: {sweep[15:12], 4'b0}
        timer_cur     [4];

        // Pre-computed write values for MEM (single write point)
        new_peak_amp   [8];
        new_peak_timer [4];
        display_wr_val [16];
        sweep_wr_val   [16];
    }

    REGISTER {
        // Latched channel state (updated each frame)
        lat_vol0   [8]  = 8'd0;   lat_vol1   [8]  = 8'd0;
        lat_vol2   [8]  = 8'd0;   lat_vol3   [8]  = 8'd0;
        lat_fund0  [7]  = 7'd127; lat_fund1  [7]  = 7'd127;
        lat_fund2  [7]  = 7'd127; lat_fund3  [7]  = 7'd127;
        lat_h3_0   [7]  = 7'd127; lat_h3_1   [7]  = 7'd127;
        lat_h3_2   [7]  = 7'd127; lat_h3_3   [7]  = 7'd127;
        lat_h5_0   [7]  = 7'd127; lat_h5_1   [7]  = 7'd127;
        lat_h5_2   [7]  = 7'd127; lat_h5_3   [7]  = 7'd127;

        // Sweep state machine
        sweep_idx  [7]  = 7'd0;
        sweeping   [1]  = 1'b0;
    }

    MEM(TYPE=DISTRIBUTED) {
        // Display: [7:0]=smooth_amp, [15:8]=peak_amp — read by display module
        display_ram [16] [128] = 16'd0 { OUT rd ASYNC; IN wr; };
        // Sweep: [7:0]=smooth, [11:8]=timer, [15:12]=peak_top4 — read by sweep
        sweep_ram   [16] [128] = 16'd0 { OUT rd ASYNC; IN wr; };
    }

    ASYNCHRONOUS {
        // ---- Read port for display ----
        rd_amp <= display_ram.rd[rd_bar];

        // ---- Effective volume ----
        ch0_vol_eff <= (ch0_in_gap == 1'b0) ? ch0_volume : 8'd0;
        ch1_vol_eff <= (ch1_in_gap == 1'b0) ? ch1_volume : 8'd0;
        ch2_vol_eff <= (ch2_in_gap == 1'b0) ? ch2_volume : 8'd0;
        ch3_vol_eff <= (ch3_in_gap == 1'b0) ? ch3_volume : 8'd0;

        // ---- Half-period to bar index mapping (ch0) ----
        IF (ch0_half_period == 16'd0 || ch0_half_period == 16'hFFFF) {
            ch0_fund <= 7'd127;  ch0_h3 <= 7'd127;  ch0_h5 <= 7'd127;
        } ELIF (ch0_half_period > 16'd231) {
            ch0_fund <= 7'd7;   ch0_h3 <= 7'd27;  ch0_h5 <= 7'd42;
        } ELIF (ch0_half_period > 16'd206) {
            ch0_fund <= 7'd9;   ch0_h3 <= 7'd29;  ch0_h5 <= 7'd44;
        } ELIF (ch0_half_period > 16'd189) {
            ch0_fund <= 7'd11;  ch0_h3 <= 7'd31;  ch0_h5 <= 7'd47;
        } ELIF (ch0_half_period > 16'd174) {
            ch0_fund <= 7'd12;  ch0_h3 <= 7'd32;  ch0_h5 <= 7'd48;
        } ELIF (ch0_half_period > 16'd155) {
            ch0_fund <= 7'd14;  ch0_h3 <= 7'd34;  ch0_h5 <= 7'd50;
        } ELIF (ch0_half_period > 16'd141) {
            ch0_fund <= 7'd17;  ch0_h3 <= 7'd37;  ch0_h5 <= 7'd52;
        } ELIF (ch0_half_period > 16'd129) {
            ch0_fund <= 7'd18;  ch0_h3 <= 7'd38;  ch0_h5 <= 7'd54;
        } ELIF (ch0_half_period > 16'd115) {
            ch0_fund <= 7'd20;  ch0_h3 <= 7'd40;  ch0_h5 <= 7'd56;
        } ELIF (ch0_half_period > 16'd103) {
            ch0_fund <= 7'd22;  ch0_h3 <= 7'd42;  ch0_h5 <= 7'd58;
        } ELIF (ch0_half_period > 16'd94) {
            ch0_fund <= 7'd24;  ch0_h3 <= 7'd44;  ch0_h5 <= 7'd60;
        } ELIF (ch0_half_period > 16'd87) {
            ch0_fund <= 7'd25;  ch0_h3 <= 7'd45;  ch0_h5 <= 7'd61;
        } ELIF (ch0_half_period > 16'd77) {
            ch0_fund <= 7'd27;  ch0_h3 <= 7'd47;  ch0_h5 <= 7'd63;
        } ELIF (ch0_half_period > 16'd71) {
            ch0_fund <= 7'd29;  ch0_h3 <= 7'd49;  ch0_h5 <= 7'd65;
        } ELIF (ch0_half_period > 16'd65) {
            ch0_fund <= 7'd30;  ch0_h3 <= 7'd50;  ch0_h5 <= 7'd66;
        } ELIF (ch0_half_period > 16'd58) {
            ch0_fund <= 7'd33;  ch0_h3 <= 7'd53;  ch0_h5 <= 7'd68;
        } ELIF (ch0_half_period > 16'd52) {
            ch0_fund <= 7'd35;  ch0_h3 <= 7'd55;  ch0_h5 <= 7'd70;
        } ELIF (ch0_half_period > 16'd47) {
            ch0_fund <= 7'd37;  ch0_h3 <= 7'd57;  ch0_h5 <= 7'd72;
        } ELIF (ch0_half_period > 16'd43) {
            ch0_fund <= 7'd38;  ch0_h3 <= 7'd58;  ch0_h5 <= 7'd74;
        } ELIF (ch0_half_period > 16'd39) {
            ch0_fund <= 7'd40;  ch0_h3 <= 7'd60;  ch0_h5 <= 7'd76;
        } ELIF (ch0_half_period > 16'd35) {
            ch0_fund <= 7'd42;  ch0_h3 <= 7'd62;  ch0_h5 <= 7'd78;
        } ELSE {
            ch0_fund <= 7'd127;  ch0_h3 <= 7'd127;  ch0_h5 <= 7'd127;
        }

        // ---- Half-period to bar index mapping (ch1) ----
        IF (ch1_half_period == 16'd0 || ch1_half_period == 16'hFFFF) {
            ch1_fund <= 7'd127;  ch1_h3 <= 7'd127;  ch1_h5 <= 7'd127;
        } ELIF (ch1_half_period > 16'd231) {
            ch1_fund <= 7'd7;   ch1_h3 <= 7'd27;  ch1_h5 <= 7'd42;
        } ELIF (ch1_half_period > 16'd206) {
            ch1_fund <= 7'd9;   ch1_h3 <= 7'd29;  ch1_h5 <= 7'd44;
        } ELIF (ch1_half_period > 16'd189) {
            ch1_fund <= 7'd11;  ch1_h3 <= 7'd31;  ch1_h5 <= 7'd47;
        } ELIF (ch1_half_period > 16'd174) {
            ch1_fund <= 7'd12;  ch1_h3 <= 7'd32;  ch1_h5 <= 7'd48;
        } ELIF (ch1_half_period > 16'd155) {
            ch1_fund <= 7'd14;  ch1_h3 <= 7'd34;  ch1_h5 <= 7'd50;
        } ELIF (ch1_half_period > 16'd141) {
            ch1_fund <= 7'd17;  ch1_h3 <= 7'd37;  ch1_h5 <= 7'd52;
        } ELIF (ch1_half_period > 16'd129) {
            ch1_fund <= 7'd18;  ch1_h3 <= 7'd38;  ch1_h5 <= 7'd54;
        } ELIF (ch1_half_period > 16'd115) {
            ch1_fund <= 7'd20;  ch1_h3 <= 7'd40;  ch1_h5 <= 7'd56;
        } ELIF (ch1_half_period > 16'd103) {
            ch1_fund <= 7'd22;  ch1_h3 <= 7'd42;  ch1_h5 <= 7'd58;
        } ELIF (ch1_half_period > 16'd94) {
            ch1_fund <= 7'd24;  ch1_h3 <= 7'd44;  ch1_h5 <= 7'd60;
        } ELIF (ch1_half_period > 16'd87) {
            ch1_fund <= 7'd25;  ch1_h3 <= 7'd45;  ch1_h5 <= 7'd61;
        } ELIF (ch1_half_period > 16'd77) {
            ch1_fund <= 7'd27;  ch1_h3 <= 7'd47;  ch1_h5 <= 7'd63;
        } ELIF (ch1_half_period > 16'd71) {
            ch1_fund <= 7'd29;  ch1_h3 <= 7'd49;  ch1_h5 <= 7'd65;
        } ELIF (ch1_half_period > 16'd65) {
            ch1_fund <= 7'd30;  ch1_h3 <= 7'd50;  ch1_h5 <= 7'd66;
        } ELIF (ch1_half_period > 16'd58) {
            ch1_fund <= 7'd33;  ch1_h3 <= 7'd53;  ch1_h5 <= 7'd68;
        } ELIF (ch1_half_period > 16'd52) {
            ch1_fund <= 7'd35;  ch1_h3 <= 7'd55;  ch1_h5 <= 7'd70;
        } ELIF (ch1_half_period > 16'd47) {
            ch1_fund <= 7'd37;  ch1_h3 <= 7'd57;  ch1_h5 <= 7'd72;
        } ELIF (ch1_half_period > 16'd43) {
            ch1_fund <= 7'd38;  ch1_h3 <= 7'd58;  ch1_h5 <= 7'd74;
        } ELIF (ch1_half_period > 16'd39) {
            ch1_fund <= 7'd40;  ch1_h3 <= 7'd60;  ch1_h5 <= 7'd76;
        } ELIF (ch1_half_period > 16'd35) {
            ch1_fund <= 7'd42;  ch1_h3 <= 7'd62;  ch1_h5 <= 7'd78;
        } ELSE {
            ch1_fund <= 7'd127;  ch1_h3 <= 7'd127;  ch1_h5 <= 7'd127;
        }

        // ---- Half-period to bar index mapping (ch2) ----
        IF (ch2_half_period == 16'd0 || ch2_half_period == 16'hFFFF) {
            ch2_fund <= 7'd127;  ch2_h3 <= 7'd127;  ch2_h5 <= 7'd127;
        } ELIF (ch2_half_period > 16'd231) {
            ch2_fund <= 7'd7;   ch2_h3 <= 7'd27;  ch2_h5 <= 7'd42;
        } ELIF (ch2_half_period > 16'd206) {
            ch2_fund <= 7'd9;   ch2_h3 <= 7'd29;  ch2_h5 <= 7'd44;
        } ELIF (ch2_half_period > 16'd189) {
            ch2_fund <= 7'd11;  ch2_h3 <= 7'd31;  ch2_h5 <= 7'd47;
        } ELIF (ch2_half_period > 16'd174) {
            ch2_fund <= 7'd12;  ch2_h3 <= 7'd32;  ch2_h5 <= 7'd48;
        } ELIF (ch2_half_period > 16'd155) {
            ch2_fund <= 7'd14;  ch2_h3 <= 7'd34;  ch2_h5 <= 7'd50;
        } ELIF (ch2_half_period > 16'd141) {
            ch2_fund <= 7'd17;  ch2_h3 <= 7'd37;  ch2_h5 <= 7'd52;
        } ELIF (ch2_half_period > 16'd129) {
            ch2_fund <= 7'd18;  ch2_h3 <= 7'd38;  ch2_h5 <= 7'd54;
        } ELIF (ch2_half_period > 16'd115) {
            ch2_fund <= 7'd20;  ch2_h3 <= 7'd40;  ch2_h5 <= 7'd56;
        } ELIF (ch2_half_period > 16'd103) {
            ch2_fund <= 7'd22;  ch2_h3 <= 7'd42;  ch2_h5 <= 7'd58;
        } ELIF (ch2_half_period > 16'd94) {
            ch2_fund <= 7'd24;  ch2_h3 <= 7'd44;  ch2_h5 <= 7'd60;
        } ELIF (ch2_half_period > 16'd87) {
            ch2_fund <= 7'd25;  ch2_h3 <= 7'd45;  ch2_h5 <= 7'd61;
        } ELIF (ch2_half_period > 16'd77) {
            ch2_fund <= 7'd27;  ch2_h3 <= 7'd47;  ch2_h5 <= 7'd63;
        } ELIF (ch2_half_period > 16'd71) {
            ch2_fund <= 7'd29;  ch2_h3 <= 7'd49;  ch2_h5 <= 7'd65;
        } ELIF (ch2_half_period > 16'd65) {
            ch2_fund <= 7'd30;  ch2_h3 <= 7'd50;  ch2_h5 <= 7'd66;
        } ELIF (ch2_half_period > 16'd58) {
            ch2_fund <= 7'd33;  ch2_h3 <= 7'd53;  ch2_h5 <= 7'd68;
        } ELIF (ch2_half_period > 16'd52) {
            ch2_fund <= 7'd35;  ch2_h3 <= 7'd55;  ch2_h5 <= 7'd70;
        } ELIF (ch2_half_period > 16'd47) {
            ch2_fund <= 7'd37;  ch2_h3 <= 7'd57;  ch2_h5 <= 7'd72;
        } ELIF (ch2_half_period > 16'd43) {
            ch2_fund <= 7'd38;  ch2_h3 <= 7'd58;  ch2_h5 <= 7'd74;
        } ELIF (ch2_half_period > 16'd39) {
            ch2_fund <= 7'd40;  ch2_h3 <= 7'd60;  ch2_h5 <= 7'd76;
        } ELIF (ch2_half_period > 16'd35) {
            ch2_fund <= 7'd42;  ch2_h3 <= 7'd62;  ch2_h5 <= 7'd78;
        } ELSE {
            ch2_fund <= 7'd127;  ch2_h3 <= 7'd127;  ch2_h5 <= 7'd127;
        }

        // ---- Half-period to bar index mapping (ch3) ----
        IF (ch3_half_period == 16'd0 || ch3_half_period == 16'hFFFF) {
            ch3_fund <= 7'd127;  ch3_h3 <= 7'd127;  ch3_h5 <= 7'd127;
        } ELIF (ch3_half_period > 16'd231) {
            ch3_fund <= 7'd7;   ch3_h3 <= 7'd27;  ch3_h5 <= 7'd42;
        } ELIF (ch3_half_period > 16'd206) {
            ch3_fund <= 7'd9;   ch3_h3 <= 7'd29;  ch3_h5 <= 7'd44;
        } ELIF (ch3_half_period > 16'd189) {
            ch3_fund <= 7'd11;  ch3_h3 <= 7'd31;  ch3_h5 <= 7'd47;
        } ELIF (ch3_half_period > 16'd174) {
            ch3_fund <= 7'd12;  ch3_h3 <= 7'd32;  ch3_h5 <= 7'd48;
        } ELIF (ch3_half_period > 16'd155) {
            ch3_fund <= 7'd14;  ch3_h3 <= 7'd34;  ch3_h5 <= 7'd50;
        } ELIF (ch3_half_period > 16'd141) {
            ch3_fund <= 7'd17;  ch3_h3 <= 7'd37;  ch3_h5 <= 7'd52;
        } ELIF (ch3_half_period > 16'd129) {
            ch3_fund <= 7'd18;  ch3_h3 <= 7'd38;  ch3_h5 <= 7'd54;
        } ELIF (ch3_half_period > 16'd115) {
            ch3_fund <= 7'd20;  ch3_h3 <= 7'd40;  ch3_h5 <= 7'd56;
        } ELIF (ch3_half_period > 16'd103) {
            ch3_fund <= 7'd22;  ch3_h3 <= 7'd42;  ch3_h5 <= 7'd58;
        } ELIF (ch3_half_period > 16'd94) {
            ch3_fund <= 7'd24;  ch3_h3 <= 7'd44;  ch3_h5 <= 7'd60;
        } ELIF (ch3_half_period > 16'd87) {
            ch3_fund <= 7'd25;  ch3_h3 <= 7'd45;  ch3_h5 <= 7'd61;
        } ELIF (ch3_half_period > 16'd77) {
            ch3_fund <= 7'd27;  ch3_h3 <= 7'd47;  ch3_h5 <= 7'd63;
        } ELIF (ch3_half_period > 16'd71) {
            ch3_fund <= 7'd29;  ch3_h3 <= 7'd49;  ch3_h5 <= 7'd65;
        } ELIF (ch3_half_period > 16'd65) {
            ch3_fund <= 7'd30;  ch3_h3 <= 7'd50;  ch3_h5 <= 7'd66;
        } ELIF (ch3_half_period > 16'd58) {
            ch3_fund <= 7'd33;  ch3_h3 <= 7'd53;  ch3_h5 <= 7'd68;
        } ELIF (ch3_half_period > 16'd52) {
            ch3_fund <= 7'd35;  ch3_h3 <= 7'd55;  ch3_h5 <= 7'd70;
        } ELIF (ch3_half_period > 16'd47) {
            ch3_fund <= 7'd37;  ch3_h3 <= 7'd57;  ch3_h5 <= 7'd72;
        } ELIF (ch3_half_period > 16'd43) {
            ch3_fund <= 7'd38;  ch3_h3 <= 7'd58;  ch3_h5 <= 7'd74;
        } ELIF (ch3_half_period > 16'd39) {
            ch3_fund <= 7'd40;  ch3_h3 <= 7'd60;  ch3_h5 <= 7'd76;
        } ELIF (ch3_half_period > 16'd35) {
            ch3_fund <= 7'd42;  ch3_h3 <= 7'd62;  ch3_h5 <= 7'd78;
        } ELSE {
            ch3_fund <= 7'd127;  ch3_h3 <= 7'd127;  ch3_h5 <= 7'd127;
        }

        // ---- Signed distances from sweep_idx to each source bar ----
        d0f  <= { 1'b0, sweep_idx } - { 1'b0, lat_fund0 };
        d0h3 <= { 1'b0, sweep_idx } - { 1'b0, lat_h3_0 };
        d0h5 <= { 1'b0, sweep_idx } - { 1'b0, lat_h5_0 };
        d1f  <= { 1'b0, sweep_idx } - { 1'b0, lat_fund1 };
        d1h3 <= { 1'b0, sweep_idx } - { 1'b0, lat_h3_1 };
        d1h5 <= { 1'b0, sweep_idx } - { 1'b0, lat_h5_1 };
        d2f  <= { 1'b0, sweep_idx } - { 1'b0, lat_fund2 };
        d2h3 <= { 1'b0, sweep_idx } - { 1'b0, lat_h3_2 };
        d2h5 <= { 1'b0, sweep_idx } - { 1'b0, lat_h5_2 };
        d3f  <= { 1'b0, sweep_idx } - { 1'b0, lat_fund3 };
        d3h3 <= { 1'b0, sweep_idx } - { 1'b0, lat_h3_3 };
        d3h5 <= { 1'b0, sweep_idx } - { 1'b0, lat_h5_3 };

        // Absolute values
        a0f  <= (d0f[7]  == 1'b1) ? (7'd0 - d0f[6:0])  : d0f[6:0];
        a0h3 <= (d0h3[7] == 1'b1) ? (7'd0 - d0h3[6:0]) : d0h3[6:0];
        a0h5 <= (d0h5[7] == 1'b1) ? (7'd0 - d0h5[6:0]) : d0h5[6:0];
        a1f  <= (d1f[7]  == 1'b1) ? (7'd0 - d1f[6:0])  : d1f[6:0];
        a1h3 <= (d1h3[7] == 1'b1) ? (7'd0 - d1h3[6:0]) : d1h3[6:0];
        a1h5 <= (d1h5[7] == 1'b1) ? (7'd0 - d1h5[6:0]) : d1h5[6:0];
        a2f  <= (d2f[7]  == 1'b1) ? (7'd0 - d2f[6:0])  : d2f[6:0];
        a2h3 <= (d2h3[7] == 1'b1) ? (7'd0 - d2h3[6:0]) : d2h3[6:0];
        a2h5 <= (d2h5[7] == 1'b1) ? (7'd0 - d2h5[6:0]) : d2h5[6:0];
        a3f  <= (d3f[7]  == 1'b1) ? (7'd0 - d3f[6:0])  : d3f[6:0];
        a3h3 <= (d3h3[7] == 1'b1) ? (7'd0 - d3h3[6:0]) : d3h3[6:0];
        a3h5 <= (d3h5[7] == 1'b1) ? (7'd0 - d3h5[6:0]) : d3h5[6:0];

        // ---- Per-source contributions (no self-reference) ----
        // Ch0 fundamental: full volume center, half +-1, quarter +-2
        IF (a0f == 7'd0) {
            c0f <= lat_vol0;
        } ELIF (a0f == 7'd1) {
            c0f <= { 1'b0, lat_vol0[7:1] };
        } ELIF (a0f == 7'd2) {
            c0f <= { 2'b00, lat_vol0[7:2] };
        } ELSE {
            c0f <= 8'd0;
        }
        // Ch0 3rd harmonic: 1/4 center, 1/8 +-1, 1/16 +-2
        IF (a0h3 == 7'd0) {
            c0h3 <= { 2'b00, lat_vol0[7:2] };
        } ELIF (a0h3 == 7'd1) {
            c0h3 <= { 3'b000, lat_vol0[7:3] };
        } ELIF (a0h3 == 7'd2) {
            c0h3 <= { 4'b0000, lat_vol0[7:4] };
        } ELSE {
            c0h3 <= 8'd0;
        }
        // Ch0 5th harmonic: 1/8 center, 1/16 +-1
        IF (a0h5 == 7'd0) {
            c0h5 <= { 3'b000, lat_vol0[7:3] };
        } ELIF (a0h5 == 7'd1) {
            c0h5 <= { 4'b0000, lat_vol0[7:4] };
        } ELSE {
            c0h5 <= 8'd0;
        }

        // Ch1 fundamental
        IF (a1f == 7'd0) {
            c1f <= lat_vol1;
        } ELIF (a1f == 7'd1) {
            c1f <= { 1'b0, lat_vol1[7:1] };
        } ELIF (a1f == 7'd2) {
            c1f <= { 2'b00, lat_vol1[7:2] };
        } ELSE {
            c1f <= 8'd0;
        }
        IF (a1h3 == 7'd0) {
            c1h3 <= { 2'b00, lat_vol1[7:2] };
        } ELIF (a1h3 == 7'd1) {
            c1h3 <= { 3'b000, lat_vol1[7:3] };
        } ELIF (a1h3 == 7'd2) {
            c1h3 <= { 4'b0000, lat_vol1[7:4] };
        } ELSE {
            c1h3 <= 8'd0;
        }
        IF (a1h5 == 7'd0) {
            c1h5 <= { 3'b000, lat_vol1[7:3] };
        } ELIF (a1h5 == 7'd1) {
            c1h5 <= { 4'b0000, lat_vol1[7:4] };
        } ELSE {
            c1h5 <= 8'd0;
        }

        // Ch2 fundamental
        IF (a2f == 7'd0) {
            c2f <= lat_vol2;
        } ELIF (a2f == 7'd1) {
            c2f <= { 1'b0, lat_vol2[7:1] };
        } ELIF (a2f == 7'd2) {
            c2f <= { 2'b00, lat_vol2[7:2] };
        } ELSE {
            c2f <= 8'd0;
        }
        IF (a2h3 == 7'd0) {
            c2h3 <= { 2'b00, lat_vol2[7:2] };
        } ELIF (a2h3 == 7'd1) {
            c2h3 <= { 3'b000, lat_vol2[7:3] };
        } ELIF (a2h3 == 7'd2) {
            c2h3 <= { 4'b0000, lat_vol2[7:4] };
        } ELSE {
            c2h3 <= 8'd0;
        }
        IF (a2h5 == 7'd0) {
            c2h5 <= { 3'b000, lat_vol2[7:3] };
        } ELIF (a2h5 == 7'd1) {
            c2h5 <= { 4'b0000, lat_vol2[7:4] };
        } ELSE {
            c2h5 <= 8'd0;
        }

        // Ch3 fundamental
        IF (a3f == 7'd0) {
            c3f <= lat_vol3;
        } ELIF (a3f == 7'd1) {
            c3f <= { 1'b0, lat_vol3[7:1] };
        } ELIF (a3f == 7'd2) {
            c3f <= { 2'b00, lat_vol3[7:2] };
        } ELSE {
            c3f <= 8'd0;
        }
        IF (a3h3 == 7'd0) {
            c3h3 <= { 2'b00, lat_vol3[7:2] };
        } ELIF (a3h3 == 7'd1) {
            c3h3 <= { 3'b000, lat_vol3[7:3] };
        } ELIF (a3h3 == 7'd2) {
            c3h3 <= { 4'b0000, lat_vol3[7:4] };
        } ELSE {
            c3h3 <= 8'd0;
        }
        IF (a3h5 == 7'd0) {
            c3h5 <= { 3'b000, lat_vol3[7:3] };
        } ELIF (a3h5 == 7'd1) {
            c3h5 <= { 4'b0000, lat_vol3[7:4] };
        } ELSE {
            c3h5 <= 8'd0;
        }

        // ---- Sum tree (no self-reference) ----
        sum_01f  <= { 1'b0, c0f }  + { 1'b0, c1f };
        sum_23f  <= { 1'b0, c2f }  + { 1'b0, c3f };
        sum_01h3 <= { 1'b0, c0h3 } + { 1'b0, c1h3 };
        sum_23h3 <= { 1'b0, c2h3 } + { 1'b0, c3h3 };
        sum_01h5 <= { 1'b0, c0h5 } + { 1'b0, c1h5 };
        sum_23h5 <= { 1'b0, c2h5 } + { 1'b0, c3h5 };

        sum_fund <= { 1'b0, sum_01f }  + { 1'b0, sum_23f };
        sum_h3   <= { 1'b0, sum_01h3 } + { 1'b0, sum_23h3 };
        sum_h5   <= { 1'b0, sum_01h5 } + { 1'b0, sum_23h5 };

        target_acc <= { 1'b0, sum_fund } + { 1'b0, sum_h3 } + { 1'b0, sum_h5 };
        IF (target_acc > 11'd255) {
            target_amp <= 8'hFF;
        } ELSE {
            target_amp <= target_acc[7:0];
        }

        // ---- Smoothing (read smooth from sweep_ram[7:0]) ----
        smooth_cur <= sweep_ram.rd[sweep_idx][7:0];
        smooth_delta <= { 1'b0, target_amp } - { 1'b0, smooth_cur };
        smooth_step  <= { smooth_delta[8], smooth_delta[8], smooth_delta[8:2] };
        smooth_next  <= { 1'b0, smooth_cur } + smooth_step;
        IF (smooth_next[8] == 1'b1) {
            smooth_result <= 8'd0;
        } ELSE {
            smooth_result <= smooth_next[7:0];
        }

        // ---- Peak read (from sweep_ram: [15:12]=peak_top4, [11:8]=timer) ----
        peak_approx <= { sweep_ram.rd[sweep_idx][15:12], 4'b0000 };
        timer_cur   <= sweep_ram.rd[sweep_idx][11:8];

        // ---- Pre-compute write values ----
        IF (smooth_result > peak_approx) {
            new_peak_amp   <= smooth_result;
            new_peak_timer <= 4'd15;
            display_wr_val <= { smooth_result, smooth_result };
        } ELIF (timer_cur == 4'd0) {
            new_peak_amp   <= smooth_result;
            new_peak_timer <= 4'd0;
            display_wr_val <= { smooth_result, smooth_result };
        } ELSE {
            new_peak_amp   <= peak_approx;
            new_peak_timer <= timer_cur - 4'd1;
            display_wr_val <= { peak_approx, smooth_result };
        }
        sweep_wr_val <= { new_peak_amp[7:4], new_peak_timer, smooth_result };
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        IF (frame_pulse == 1'b1 && sweeping == 1'b0) {
            // Latch volumes and bar indices
            lat_vol0  <= ch0_vol_eff;
            lat_vol1  <= ch1_vol_eff;
            lat_vol2  <= ch2_vol_eff;
            lat_vol3  <= ch3_vol_eff;
            lat_fund0 <= ch0_fund;  lat_fund1 <= ch1_fund;
            lat_fund2 <= ch2_fund;  lat_fund3 <= ch3_fund;
            lat_h3_0  <= ch0_h3;    lat_h3_1  <= ch1_h3;
            lat_h3_2  <= ch2_h3;    lat_h3_3  <= ch3_h3;
            lat_h5_0  <= ch0_h5;    lat_h5_1  <= ch1_h5;
            lat_h5_2  <= ch2_h5;    lat_h5_3  <= ch3_h5;
            sweeping  <= 1'b1;
            sweep_idx <= 7'd0;
        } ELIF (sweeping == 1'b1) {
            // Write both RAMs (single write per cycle)
            sweep_ram.wr[sweep_idx]   <= sweep_wr_val;
            display_ram.wr[sweep_idx] <= display_wr_val;

            IF (sweep_idx == 7'd79) {
                sweeping <= 1'b0;
            } ELSE {
                sweep_idx <= sweep_idx + 7'd1;
            }
        }
    }
@endmod
jz
// Spectrum Display — 80-Bar Renderer
// Reads packed bar state from DISTRIBUTED RAM (smooth+peak amplitudes),
// renders 80 narrow pill-shaped bar graphs with reflections and peak hold.
//
// Bar layout: 80 bars, 16px pitch (10px body + 6px gap) = 1280px total.
// Bars grow upward from baseline at y=600. Max height: 25 segments (400px).
// Segment: 12px body + 4px gap = 16px pitch.
// Reflection: below baseline, up to 6 segments, 1/4 brightness.
// Peak hold: single bright segment above bar (SHOW_PEAK=1 to enable).
// Color: smooth gradient red(bar 0) -> yellow(bar 39) -> blue(bar 79).
@module spectrum_display
    CONST {
        SHOW_PEAK = 0;    // 1=draw peak hold pill above bar, 0=disable
    }

    PORT {
        IN  [1]  clk;
        IN  [1]  rst_n;
        IN  [11] x_pos;
        IN  [10] y_pos;
        // RAM read interface
        OUT [7]  rd_bar;          // bar index to read (0..79)
        IN  [16] rd_amp;          // [7:0]=smooth_amp, [15:8]=peak_amp
        // Pixel output
        OUT [8]  red;
        OUT [8]  green;
        OUT [8]  blue;
    }

    WIRE {
        // Bar detection from x_pos
        bar_idx      [7];     // x_pos[10:4] = 0..79
        bar_x        [4];     // x_pos[3:0] = 0..15
        in_body      [1];     // bar_x < 10
        in_bar       [1];     // valid bar and in body

        // Vertical geometry
        above_base   [1];
        below_base   [1];
        pix_above    [10];    // pixels above baseline (0..399)
        pix_below    [10];    // pixels below baseline (0..99)

        // Segment computation (above baseline)
        seg_idx      [5];     // segment index (0..24)
        y_in_seg     [4];     // pixel within segment (0..15)
        in_pill      [1];     // y_in_seg < 12

        // Reflection segment (below baseline)
        ref_seg_idx  [5];
        ref_y_in_seg [4];
        in_ref_pill  [1];

        // Bar state from RAM
        smooth_amp   [8];
        peak_amp     [8];

        // Amplitude to segments
        bar_segs_raw [5];
        bar_segs     [5];
        peak_segs_raw [5];
        peak_segs    [5];
        ref_segs     [5];

        // Color gradient computation
        // half_pos: position within current color half (0-39)
        half_pos     [6];
        // ramp = half_pos * 13 >> 1 (approximates half_pos * 255/39)
        ramp_x13     [10];    // half_pos * 13, max 39*13=507
        ramp         [8];     // ramp_x13 >> 1, capped at 255

        // Color palette (computed from gradient)
        base_r       [8];
        base_g       [8];
        base_b       [8];

        // Pixel classification
        is_bar_seg   [1];
        is_peak_seg  [1];
        is_ref_seg   [1];

        // Combinational RGB
        next_r       [8];
        next_g       [8];
        next_b       [8];
    }

    REGISTER {
        red_r   [8] = 8'd0;
        green_r [8] = 8'd0;
        blue_r  [8] = 8'd0;
    }

    ASYNCHRONOUS {
        // ---- Bar column detection ----
        bar_idx <= x_pos[10:4];
        bar_x   <= x_pos[3:0];
        in_body <= (bar_x < 4'd10) ? 1'b1 : 1'b0;
        in_bar  <= (bar_idx < 7'd80 && in_body == 1'b1) ? 1'b1 : 1'b0;

        // Drive RAM read address
        rd_bar <= bar_idx;

        // Extract bar state from packed RAM value
        smooth_amp <= rd_amp[7:0];
        peak_amp   <= rd_amp[15:8];

        // ---- Amplitude to segments ----
        bar_segs_raw  <= smooth_amp[7:3];
        IF (bar_segs_raw > 5'd25) {
            bar_segs <= 5'd25;
        } ELSE {
            bar_segs <= bar_segs_raw;
        }
        peak_segs_raw <= peak_amp[7:3];
        IF (peak_segs_raw > 5'd25) {
            peak_segs <= 5'd25;
        } ELSE {
            peak_segs <= peak_segs_raw;
        }
        ref_segs <= { 2'b00, bar_segs[4:2] };

        // ---- Vertical geometry ----
        above_base <= (y_pos >= 10'd200 && y_pos < 10'd600) ? 1'b1 : 1'b0;
        below_base <= (y_pos >= 10'd600 && y_pos < 10'd700) ? 1'b1 : 1'b0;

        pix_above <= 10'd599 - y_pos;
        pix_below <= y_pos - 10'd600;

        seg_idx      <= pix_above[8:4];
        y_in_seg     <= pix_above[3:0];
        ref_seg_idx  <= pix_below[8:4];
        ref_y_in_seg <= pix_below[3:0];

        in_pill     <= (y_in_seg < 4'd12) ? 1'b1 : 1'b0;
        in_ref_pill <= (ref_y_in_seg < 4'd12) ? 1'b1 : 1'b0;

        // ---- Smooth color gradient: red(0) -> yellow(39) -> blue(79) ----
        // half_pos = position within current half (0-39)
        IF (bar_idx < 7'd40) {
            half_pos <= bar_idx[5:0];
        } ELSE {
            half_pos <= bar_idx[5:0] - 6'd40;
        }

        // ramp = half_pos * 13 >> 1 ≈ half_pos * 6.5 (maps 0-39 to 0-253)
        // half_pos * 13 = half_pos * 8 + half_pos * 4 + half_pos * 1
        ramp_x13 <= { 4'b0000, half_pos } + { 3'b000, half_pos, 1'b0 } +
                     { 2'b00, half_pos, 2'b00 } + { 1'b0, half_pos, 3'b000 };
        IF (ramp_x13[9:1] > 9'd255) {
            ramp <= 8'hFF;
        } ELSE {
            ramp <= ramp_x13[8:1];
        }

        // Bars 0-39: red -> yellow (R=FF, G ramps up, B=00)
        // Bars 40-79: yellow -> blue (R ramps down, G ramps down, B ramps up)
        IF (bar_idx < 7'd40) {
            base_r <= 8'hFF;
            base_g <= ramp;
            base_b <= 8'h00;
        } ELSE {
            base_r <= 8'hFF - ramp;
            base_g <= 8'hFF - ramp;
            base_b <= ramp;
        }

        // ---- Pixel classification ----
        is_bar_seg  <= (in_bar == 1'b1 && above_base == 1'b1 && in_pill == 1'b1 && seg_idx < bar_segs) ? 1'b1 : 1'b0;
        is_peak_seg <= (in_bar == 1'b1 && above_base == 1'b1 && in_pill == 1'b1 && seg_idx == peak_segs && peak_segs > bar_segs) ? lit(1, SHOW_PEAK) : 1'b0;
        is_ref_seg  <= (in_bar == 1'b1 && below_base == 1'b1 && in_ref_pill == 1'b1 && ref_seg_idx < ref_segs) ? 1'b1 : 1'b0;

        // ---- Combinational pixel output ----
        IF (is_bar_seg == 1'b1) {
            next_r <= base_r;
            next_g <= base_g;
            next_b <= base_b;
        } ELIF (is_peak_seg == 1'b1) {
            // Peak: brighter version (+0x40, saturate at 0xFF)
            IF (base_r > 8'hBF) {
                next_r <= 8'hFF;
            } ELSE {
                next_r <= base_r + 8'h40;
            }
            IF (base_g > 8'hBF) {
                next_g <= 8'hFF;
            } ELSE {
                next_g <= base_g + 8'h40;
            }
            IF (base_b > 8'hBF) {
                next_b <= 8'hFF;
            } ELSE {
                next_b <= base_b + 8'h40;
            }
        } ELIF (is_ref_seg == 1'b1) {
            // Reflection: 1/4 brightness
            next_r <= { 2'b00, base_r[7:2] };
            next_g <= { 2'b00, base_g[7:2] };
            next_b <= { 2'b00, base_b[7:2] };
        } ELSE {
            next_r <= 8'h00;
            next_g <= 8'h00;
            next_b <= 8'h00;
        }

        // Registered output
        red   <= red_r;
        green <= green_r;
        blue  <= blue_r;
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        red_r   <= next_r;
        green_r <= next_g;
        blue_r  <= next_b;
    }
@endmod
jz
// Button Debounce Module
// 2FF synchronizer + timing-based debounce + edge detect.
// Outputs a single-cycle pulse on each physical button press.
// Default debounce window ~20ms at 37.125 MHz (~742,500 cycles).
@module debounce
    CONST {
        DEBOUNCE_COUNT = 742500;
        COUNTER_BITS   = clog2(DEBOUNCE_COUNT);
        COUNTER_MAX    = DEBOUNCE_COUNT - 1;
    }

    PORT {
        IN   [1] clk;
        IN   [1] rst_n;
        IN   [1] btn_in;     // Raw button input (active-low)
        OUT  [1] btn_press;   // Single-cycle pulse on press
    }

    REGISTER {
        // 2FF synchronizer
        sync1  [1] = 1'b1;
        sync2  [1] = 1'b1;

        // Debounce state
        stable    [1]            = 1'b1;
        counter   [COUNTER_BITS] = COUNTER_BITS'b0;

        // Edge detect
        stable_prev [1] = 1'b1;
    }

    ASYNCHRONOUS {
        // Falling edge of debounced signal = button press (active-low)
        btn_press <= stable_prev & ~stable;
    }

    SYNCHRONOUS(CLK=clk RESET=rst_n RESET_ACTIVE=Low) {
        // 2FF synchronizer for metastability
        sync1 <= btn_in;
        sync2 <= sync1;

        // Edge detect register
        stable_prev <= stable;

        // Debounce logic
        IF (sync2 != stable) {
            IF (counter == lit(COUNTER_BITS, COUNTER_MAX)) {
                stable  <= sync2;
                counter <= COUNTER_BITS'b0;
            } ELSE {
                counter <= counter + COUNTER_BITS'b1;
            }
        } ELSE {
            counter <= COUNTER_BITS'b0;
        }
    }
@endmod
jz
@module por
    PORT {
        IN  [1] clk;
        IN  [1] done;
        OUT [1] por_n;
    }

    CONST {
        POR_CYCLES   = 1_048_576;  // ~28ms at 37.125MHz — wait for PLL lock
        POR_CNT_BITS = clog2(POR_CYCLES);
        POR_MAX      = POR_CYCLES - 1;
    }

    REGISTER {
        por_reg [1] = 1'b0;
        cnt     [POR_CNT_BITS] = POR_CNT_BITS'b0;
    }

    ASYNCHRONOUS {
        por_n <= por_reg;
    }

    SYNCHRONOUS(CLK=clk) {
        IF (done == 1'b0) {
            por_reg <= 1'b0;
            cnt <= POR_CNT_BITS'b0;
        } ELIF (cnt == lit(POR_CNT_BITS, POR_MAX)) {
            por_reg <= 1'b1;
            cnt <= cnt;
        } ELSE {
            por_reg <= 1'b0;
            cnt <= cnt + POR_CNT_BITS'b1;
        }
    }
@endmod

JZ-HDL Language Features

OVERRIDE for parameterized instances. A single module definition serves all four tone generators. OVERRIDE replaces Verilog's parameter passing with a mechanism that propagates through the module hierarchy — the override on tone_gen automatically reaches the melodies instance inside it, which controls the @file BRAM initializer. Verilog would require explicit parameter threading through every intermediate module.

Intrinsic signed arithmetic. sadd performs sign-extended addition with automatic width promotion. In Verilog, mixing signed and unsigned arithmetic requires careful $signed() casts that are easy to get wrong.

Memory type control. MEM(TYPE=BLOCK) for tone data and MEM(TYPE=DISTRIBUTED) for the spectrum analyzer's small lookup tables give the designer explicit control over BRAM vs. LUT RAM inference. Verilog leaves this to synthesis heuristics that vary by vendor.