Skip to content

MIXI-CUT Decoder Implementation Guide ​

How to implement a MIXI-CUT decoder from scratch in any language.

Architecture Overview ​

A MIXI-CUT decoder is a three-stage audio processing pipeline:

Stereo Audio → [Bandpass Filter] → [Phase-Locked Loop] → [Mass-Spring-Damper] → Speed, Lock, Position
                     ↓ L/R               ↓ frequency         ↓ smoothing
              Reject noise          Track carrier         Physical platter model

Each stage operates sample-by-sample (bandpass, PLL) or block-by-block (mass-spring, position output).

Stage 1: Bandpass Filter ​

Purpose ​

Reject everything except the 3 kHz carrier: rumble (< 100 Hz), hum (50/60 Hz), and hiss (> 10 kHz).

Implementation ​

Standard biquad bandpass filter (2nd order IIR):

H(z) = b0 * (1 - z^-2) / (1 + a1*z^-1 + a2*z^-2)

Parameters:

  • Center frequency: 3000 Hz
  • Q factor: 2.5
  • Sample rate: 44100 Hz
python
# Python reference
w0 = 2π * 3000 / 44100
alpha = sin(w0) / (2 * 2.5)
a0 = 1 + alpha

b0 = alpha / a0
b2 = -alpha / a0
a1 = -2 * cos(w0) / a0
a2 = (1 - alpha) / a0

# Process one sample
def tick(x):
    y = b0 * x + z1
    z1 = -a1 * y + z2
    z2 = b2 * x - a2 * y
    return y

You need two bandpass filters: one for each channel (L and R).

Stage 2: Phase-Locked Loop (PLL) ​

Purpose ​

Track the instantaneous frequency of the quadrature carrier and extract the playback speed.

How it works ​

  1. Compute the phase error between the input signal and the PLL's internal oscillator
  2. Apply a PI (proportional-integral) controller to adjust the oscillator frequency
  3. The ratio pll.freq / carrier_freq gives the instantaneous speed

Parameters ​

ParameterValueDescription
Center frequency3000 HzExpected carrier
Bandwidth8% (240 Hz)Tracking range
Lock time constant50 msEMA for lock quality
Amplitude gate0.005Coast below this level
Integral drain0.98/sampleDecay when unlocked

Implementation ​

python
# Phase error
err = atan2(left_filtered, right_filtered) - pll_phase

# Wrap to [-π, π]
if err > π: err -= 2π
if err < -π: err += 2π

# PI controller
integral = integral * drain + err * ki
freq = center + err * kp * sr + integral

# Update oscillator phase
phase += 2π * freq / sr
if phase >= 2π: phase -= 2π

# Lock quality (EMA of cos(error))
lock = lock * (1-α) + cos(err) * α

# Output
speed = freq / center  # 1.0 = normal play

DJ Resilience Features (v3) ​

  1. Integral drain: When lock < 0.3, multiply integral by 0.98 each sample. Prevents bias from needle drops.
  2. Amplitude gate: When signal amplitude < 0.005 (-46 dBFS), coast — don't update PLL.
  3. Frequency clamp: Limit PLL frequency to [-2x, +3x] of carrier to prevent runaway.

Stage 3: Mass-Spring-Damper ​

Purpose ​

Smooth the PLL's noisy speed output to match the physical behavior of a turntable platter (Technics SL-1200 model).

The model ​

The output speed is a weighted average between the current speed and the PLL input:

speed = speed * (1 - traction) + pll_speed * traction

Where traction varies by context:

ContextTractionHow detected
Normal play0.05 (= 1 - 0.95 inertia)Default
Scratch1.0 (instant snap)Speed delta > 0.3x
Stop0.5`
Vinyl brake0.5–0.9 (ramp)Sustained deceleration > 3 blocks
Dead zone→ 0.0`

Brake detection (v0.2) ​

v0.3.1 replaces the linear ramp below with a 3-regime state machine (DECEL → SNAP → RELEASE) in the Python reference decoder. The C and Rust ports still ship the v0.2 linear ramp documented here.

python
# Count consecutive deceleration blocks
if input < prev_input - 0.001 and speed > 0.05:
    decel_count += 1
else:
    decel_count = max(0, decel_count - 2)

# Aggressive traction when brake detected
if decel_count > 3:
    factor = min((decel_count - 3) / 5.0, 1.0)
    traction = 0.5 + factor * 0.4  # ramps to 0.9

Signal-aware feeding ​

Before feeding the PLL speed to the mass-spring, check the signal RMS:

python
speed_input = pll_speed if rms > 0.01 else 0.0

This ensures the decoder outputs 0.0 during silence (lead-in, lead-out, needle lift).

Block Processing ​

Process audio in blocks of 128 samples (2.9 ms at 44.1 kHz):

python
def process_block(left_128, right_128):
    speed_sum = lock_sum = energy_sum = 0

    for i in range(128):
        fl = bandpass_l.tick(left_128[i])
        fr = bandpass_r.tick(right_128[i])
        speed, lock = pll.tick(fl, fr)
        position += pll.freq / sample_rate

        speed_sum += speed
        lock_sum += lock
        energy_sum += fl*fl + fr*fr

    avg_speed = speed_sum / 128
    avg_lock = lock_sum / 128
    rms = sqrt(energy_sum / 128)

    output_speed = mass_spring.tick(avg_speed if rms > 0.01 else 0.0)
    output_position = position / carrier_freq  # in seconds

    return output_speed, avg_lock, output_position

Position Decoding ​

Status: the reference decoders in this repo (Python, C, Rust, JS) expose a position output that is the integral of the PLL frequency — i.e. a cumulative time counter, not the absolute position decoded from the groove. Absolute position frames are written into the signal by the encoder and can be recovered bit-by-bit with mixi_cut.encoder.decode_position_bits, but no shipping decoder yet performs frame-level acquisition from audio. The format below is what any new decoder should target.

Frame format (v0.3) ​

Every 4250 carrier cycles (~1.417 s at 33⅓ RPM), an 85-bit position frame is modulated. Bits are laid down one per 50 carrier cycles:

[13-bit Barker-13 sync] [24-bit position (cs)] [16-bit CRC-16] [32-bit RS parity]
     = 13 sync bits + 3 data + 2 CRC + 4 RS bytes * 8 = 85 bits

On the inner groove (position > 300 s) frames are laid at double density (every 2100 cycles, snapped to the 50-cycle lattice) for better SNR.

Missing cycle modulation ​

  • Bit 0: normal carrier amplitude
  • Bit 1: carrier amplitude dips to 25% for one cycle (with raised-cosine transition)

Detecting bits ​

  1. Compute the envelope of the carrier signal over consecutive cycles.
  2. Every 50 carrier cycles, compare the amplitude:
    • Amplitude < 60% of the local mean → bit is 1
    • Otherwise → bit is 0
  3. Slide a 13-bit Barker correlator over the bitstream to find frame boundaries (peak correlation ≥ 11/13 is a strong hit).
  4. Strip the sync word, extract the 9 payload bytes, verify CRC-16, then RS(4) syndromes.

Barker-13 sync word ​

Sequence: [1 1 1 1 1 0 0 1 1 0 1 0 1]. Autocorrelation sidelobes are ≤ 1/13, which survives the noise and dropouts typical of lathe-cut vinyl.

CRC-16 fast-reject ​

Polynomial 0x8005, init 0xFFFF, no reflection, no final XOR — a MIXI-CUT-specific configuration (not a standard named variant). Computed over the 3 position bytes. Allows O(1) rejection of corrupted frames before running the more expensive Reed-Solomon syndrome check.

Reed-Solomon validation ​

GF(2^8) with primitive polynomial 0x11D. Codeword = 3 data + 2 CRC + 4 parity = 9 bytes. Compute 4 syndromes over the full codeword; if all are zero the frame is valid.

Reference Implementations ​

LanguageLocationLOCNotes
Pythonsrc/mixi_cut/decoder.py300Reference, fully tested
Cdecoder_c/src/mixi_decoder.c240Zero-alloc, C99
Rustdecoder_rust/src/lib.rs350wasm-ready
JavaScriptdocs/demo/app.js150Web Audio API

Testing Your Decoder ​

Generate test signals ​

bash
# Clean signal (should give speed=1.0, lock>0.9)
mixi-cut generate --duration 10 --output test.wav

# With noise (should still lock)
# Add noise in your test code at -20 dB SNR

# Variable speed (pitch up/down)
# Generate at normal speed, then resample to simulate pitch

Expected behavior ​

InputExpected speedExpected lockNotes
Normal play1.000 ±0.005> 0.9Steady state
+8% pitch1.080 ±0.01> 0.9DJ pitch fader
Full stop0.000< 0.3Should reach 0 in <3ms
Reverse-1.000 ±0.01> 0.8Negative speed
Silence0.000decayingSignal-aware coast
33% SNR1.000 ±0.02> 0.7Noisy environment

Released under the MIT License.