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 modelEach 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 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 yYou 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
- Compute the phase error between the input signal and the PLL's internal oscillator
- Apply a PI (proportional-integral) controller to adjust the oscillator frequency
- The ratio
pll.freq / carrier_freqgives the instantaneous speed
Parameters
| Parameter | Value | Description |
|---|---|---|
| Center frequency | 3000 Hz | Expected carrier |
| Bandwidth | 8% (240 Hz) | Tracking range |
| Lock time constant | 50 ms | EMA for lock quality |
| Amplitude gate | 0.005 | Coast below this level |
| Integral drain | 0.98/sample | Decay when unlocked |
Implementation
# 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 playDJ Resilience Features (v3)
- Integral drain: When
lock < 0.3, multiply integral by 0.98 each sample. Prevents bias from needle drops. - Amplitude gate: When signal amplitude < 0.005 (-46 dBFS), coast — don't update PLL.
- 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 * tractionWhere traction varies by context:
| Context | Traction | How detected |
|---|---|---|
| Normal play | 0.05 (= 1 - 0.95 inertia) | Default |
| Scratch | 1.0 (instant snap) | Speed delta > 0.3x |
| Stop | 0.5 | ` |
| Vinyl brake | 0.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.
# 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.9Signal-aware feeding
Before feeding the PLL speed to the mass-spring, check the signal RMS:
speed_input = pll_speed if rms > 0.01 else 0.0This 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):
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_positionPosition Decoding
Status: the reference decoders in this repo (Python, C, Rust, JS) expose a
positionoutput 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 withmixi_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 bitsOn 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
- Compute the envelope of the carrier signal over consecutive cycles.
- Every 50 carrier cycles, compare the amplitude:
- Amplitude < 60% of the local mean → bit is
1 - Otherwise → bit is
0
- Amplitude < 60% of the local mean → bit is
- Slide a 13-bit Barker correlator over the bitstream to find frame boundaries (peak correlation ≥ 11/13 is a strong hit).
- 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
| Language | Location | LOC | Notes |
|---|---|---|---|
| Python | src/mixi_cut/decoder.py | 300 | Reference, fully tested |
| C | decoder_c/src/mixi_decoder.c | 240 | Zero-alloc, C99 |
| Rust | decoder_rust/src/lib.rs | 350 | wasm-ready |
| JavaScript | docs/demo/app.js | 150 | Web Audio API |
Testing Your Decoder
Generate test signals
# 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 pitchExpected behavior
| Input | Expected speed | Expected lock | Notes |
|---|---|---|---|
| Normal play | 1.000 ±0.005 | > 0.9 | Steady state |
| +8% pitch | 1.080 ±0.01 | > 0.9 | DJ pitch fader |
| Full stop | 0.000 | < 0.3 | Should reach 0 in <3ms |
| Reverse | -1.000 ±0.01 | > 0.8 | Negative speed |
| Silence | 0.000 | decaying | Signal-aware coast |
| 33% SNR | 1.000 ±0.02 | > 0.7 | Noisy environment |