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 = * 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 -=
if err < -π: err +=

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

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

# 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.