Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added content/images/avatars/jclsn.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions content/images/news/serato-control-cd.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions content/images/news/traktor-mk2-signal-with-derivative.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions content/images/news/traktor-mk2-signal-with-readings.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions content/images/news/traktor-mk2-signal-with-timecodes.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions content/images/news/traktor-mk2-signal.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
224 changes: 224 additions & 0 deletions content/news/XXXX-XX-XX-dvs-internals-pt3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
title: "How Does Timecode Vinyl Actually Work? (Pt. 3)"
authors: Jan Claußen
tags: traktor, timecode, dvs, vinyl control
status: draft
math: yes

Since its release in 2011, the **Traktor Control Vinyl MK2** has sparked
curiosity among digital DJs and audio developers alike. Its timecode format
stands apart from Serato’s, which we explored in the previous posts. With the
MK2 system, Native Instruments introduced a more advanced timecode that boosts
resolution and accuracy by applying advanced cryptographic techniques.

In this post, we’ll break down how it works at a basic level and how Mixxx is processing the signal.

---

## Recap: How Serato Timecode Works

Serato’s timecode is built around a
[Linear Feedback Shift Register](https://en.wikipedia.org/wiki/Linear-feedback_shift_register),
modulated onto a 1 kHz carrier using
[amplitude modulation (AM)](https://en.wikipedia.org/wiki/Amplitude_modulation) - a legacy
technique from radio transmission.

The demodulation process is relatively simple: when one stereo channel crosses
the x-axis, the other hits a peak. If that peak exceeds a certain threshold,
the system reads it as a **1**, if not, it’s a **0**.

![Serato Timecode Signal]({static}/images/news/serato-control-cd.svg)

We covered this in more detail in
[DVS Internals Pt. 1]({filename}/news/2021-11-21-dvs-internals-pt1.md)
and [Pt. 2]({filename}/news/2021-12-22-dvs-internals-pt2.md).

---

## The Traktor MK2 Signal

Below is a signal that resembles what you’ll find on the Traktor MK2
Control vinyl/CD, which has been specifically generated for this blog post by
using a [Raised-Cosine Filter](https://de.wikipedia.org/wiki/Raised-Cosine-Filter) to
modulate a random sequence onto the carrier.

The carrier wave operates at **2500 Hz**, a significant increase from Serato’s
**1000 Hz**.

> **Advantage:** The higher carrier frequency allows for 2.5× greater resolution.

![Offset-modulated Signal]({static}/images/news/traktor-mk2-signal.svg)

Upon inspection, this waveform clearly doesn’t use amplitude modulation - the
amplitude remains constant. Instead, it appears to be **offset-modulated**,
where the signal is shifted vertically from the x-axis. This is a non-standard
technique not commonly used in typical modulation schemes.

On the original vinyl version (not shown here due to copyright), the offset
can be so large that the signal floats entirely above the x-axis for multiple
cycles - making zero-crossing detection impossible.

Even when that doesn’t happen, the offset causes the time interval $\Delta t$
between zero-crossings to become irregular, introducing audible pitch flutter.

To decode the signal, we must solve:

1. How to filter the signal to enable pitch detection
2. How to demodulate this non-standard modulation
3. How to decipher the code that is modulated onto the carrier

---

## Pitch Detection

> **Note:** If you're unfamiliar with pitch detection in DVS systems, revisit
> [DVS Internals Pt. 1]({filename}/news/2021-11-21-dvs-internals-pt1.md).

Our goal is to produce a signal that oscillates evenly around the x-axis. This
filtered waveform can then be processed by the existing pitch detection algorithm.

A simple discrete derivative operation achieves this:

$$
y[n] = x[n] - x[n-1] \tag{1}
$$

$\text{where:}$<br>
$\text{- x[n]: Input sample}$<br>
$\text{- x[n-1]: Delayed input sample}$<br>
$\text{- y[n]: Difference of both values}$<br>
<br>

When applied to the offset-modulated signal, we get:

![Traktor MK2 signal with derivative]({static}/images/news/traktor-mk2-signal-with-derivative.svg)

The resulting waveform oscillates cleanly around zero, which is ideal for
analysis. It also makes it easier to pinpoint the half-cycle peaks needed for
bit detection.

---

## Demodulation Techniques

To extract bits from the signal, we detect the zero-crossings and sample the
amplitude of the sinusoid at those moments.

![Traktor MK2 signal with
zero-crossings]({static}/images/news/traktor-mk2-signal-with-zero-crossings.svg)

You may notice that the derivative’s zero-crossings don’t align perfectly with
the original peaks. That’s due to a delay introduced by the filter. Smoothing
the signal first, then compensating for the delay (e.g., by selecting
$x[n-3]$), yields better results.

For greater accuracy, one could analyze the phase response $\phi(\omega)$,
which shows how filter delay varies with input frequency - but for this use case,
a fixed delay works well enough.

The filtered signal can cross the x-axis in two directions-positive to
negative or vice versa. Based on the direction, we determine which half-cycle
contains the encoded bit. In this example we only use the upper half-cycle for
decoding, but you can also decode the signal from the lower half-cycle.

![Traktor MK2 signal with readings]({static}/images/news/traktor-mk2-signal-with-readings.svg)

Demodulation is then as simple as applying a threshold: amplitudes above it
are **1**, and below it are **0**.

![Traktor MK2 signal with timecodes]({static}/images/news/traktor-mk2-signal-with-timecodes.svg)

On actual vinyl, the physical behavior of the needle causes the offset to
decay over time, because the needle slowly drifts back to the middle. This decay complicates bit extraction.

To compensate, we analyze the **slope** between subsequent readings by
reusing the derivative equation in $\text{(1)}$.
Comment thread
jclsn marked this conversation as resolved.

$$slope[n] = reading[n] - reading[n-1]$$

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

latex in generally typesets strings of characters as their own symbols. To make this render nicely you'll need to wrap these in \text{}...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what signal does reading actually refer to?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you visualize the new slope signal too?

@jclsn jclsn Jul 28, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The derivative is the "slope signal". What I can do is to draw an arrow between to readings to make it more clear. Readings are the values where half-cycles peak. They are visualized by the red dots.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, so its just the $y[n] = x[n] + x[n-1]$ equation but with $x$ and $y$ renamed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, x is every sample of the sinusoid. The readings are only those samples, which are located at wave peaks. So readings is a subset of x. Only the formula is the same. Visualizing the slope as a continuous signal with the sparse reading data points, won't make much sense. The slope is merely a value between -1.0...1.0.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it makes total sense. I just didn't make the connection that its the same formula and you just renamed the variables. Personally, I would eliminate the second formula and just explain that x is the reading and y is the slope (for those that aren't familiar enough with calculus to make that connection themselves).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I should leave that part out completely. I can't show the actual signal anyway for copyright reasons. It's hard to imagine why this is necessary when you don't see the actual signal.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure you can't show a small sample of the signal? The actual audio data that could be recovered from that would be so small that I doubt it wouldn't fall under fair use (or similar).

@jclsn jclsn Aug 27, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just want to make sure to not get any problems. For understanding the inner workings of the timecode, the current explanations should suffice. I also generated a signal using a Raised-Cosine filter, which is very close to what you'd find on the CD.


$\text{where:}$<br>
$\text{- x[n]: Current reading}$<br>
$\text{- x[n-1]: Last reading}$<br>
$\text{- y[n]: Difference of both values}$<br>
<br>

We then decode a positive slope to toggle the bit to **1** and a negative
slope to toggle the bit to **0**.

![Traktor MK2 signal with indicated slope]({static}/images/news/traktor-mk2-signal-with-slope.png)

This method helps isolate the encoded signal from the floating zero line
caused by mechanical drift.

---

## The Code

> **Note:** A deeper explanation of LFSRs can be found in [DVS Internals Pt. 2]({filename}/news/2021-12-22-dvs-internals-pt2.md)

Interestingly, the Traktor MK2 system also uses a [Linear Feedback Shift Register](https://en.wikipedia.org/wiki/Linear-feedback_shift_register) - but with different properties. While Serato’s LFSR has a
**20-bit** length, Traktor’s uses a **110-bit** register with a minimum
run length of two symbols. The generator polynomial of the the LFSR was found
by using the [Berlekamp-Massey algorithm](https://en.wikipedia.org/wiki/Berlekamp%E2%80%93Massey_algorithm).

The number of unique states an LFSR can generate is:

$$n_{max} = 2^m -1$$

Hence for the Serato timecode

$$n_{serato} = 2^{20} -1 = 1\,048\,575$$

and for the Traktor MK2 timecode

$$n_{mk2} = 2^{110} -1 = 1.298 \cdot 10^{33} = 1\,298\,074\,214\,633\,706\,907\,132\,624\,082\,305\,023$$

That’s an astronomically high number-far beyond what’s required for this
application.

But how many states are actually needed? With a 2500 Hz carrier, you get 2500 bits per second.<br>

For 12 minutes of timecode:

$$12 \text{ min} \cdot 60 = 720 \text{ s}$$
$$720 \text{ s} \cdot 2500 \text{ states/s} = 1\,800\,000 \text{ states}$$

which exceeds the maximum state range of Serato’s 20-bit LFSR by far.

However, a downside appears: each 110-bit state must be stored in 128 bits (4
× 32-bit integers).

So for the A-side with 12 minutes:

$$1\,800\,000 \text{ states} \cdot 128 \text{ bit} = 230\,400\,000 \text{ bit} = 28\,800\,000 \text{ byte} = 27.46 \text{ MB}$$

And for a 25-minute CD:

$$4\,500\,000 \text{ states} \cdot 128 \text{ bit} = 576\,000\,000 \text{ bit} = 72\,000\,000 \text{ byte} = 68.66 \text{ MB}$$

> **Disadvantage:** The memory footprint is large-even a single side of timecode can exceed 27 MB.

This makes storing a full lookup table impractical in production software.

It's important to point out that the current implementation is naive, because it treats the
Traktor MK2 code as if it were Serato code. Since Mark Hills designed the xwax
library, which is used by vinyl control in Mixxx, for exactly this style of
timecode, changes would have to be made to make the decoder more modular.

Nonetheless, the current technique works and it represents the current state
of the decoder in Mixxx.

---

## Conclusion

Fortunately, there are mathematical methods to reduce the memory requirements.
This requires diving deeper into the crypthographic theory.

First tests show that this can possibly be achieved by applying a fixed tap
pattern (e.g. every 5th bit) to a 110-bit LFSR window - a form of structured
decimation or undersampling. This collapses the sequence into a 22-bit
[Gold code](https://en.wikipedia.org/wiki/Gold_code), whose two sequences alternate.
The implementation of this technique is far more complex and not completed
yet.

We’ll explore those strategies in the next part of this series.
4 changes: 2 additions & 2 deletions netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,5 @@
[[headers]]
for = "/news/*"
[headers.values]
# Additionally allow YouTube/Discourse frames and scripts
Content-Security-Policy = "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'sha256-UPkidoMErzWw1gW/eY4LhAi9ZkPch3PP31d6KQoJ6Yc=' 'sha256-G40wI6OaLZXCtrb02xUq1H1kEVWjstzoQ0FXKwsWxPw=' https://mixxx.discourse.group/javascripts/embed.js *.discourse-cdn.com; frame-src 'self' https://www.youtube-nocookie.com https://mixxx.discourse.group ; img-src 'self' https://i.ytimg.com https://raw.githubusercontent.com/mixxxdj/ ; connect-src 'self' https://mixxx.discourse.group https://*.discourse-cdn.com"
# Additionally allow YouTube/Discourse frames and scripts and MathJax
Content-Security-Policy = "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'sha256-UPkidoMErzWw1gW/eY4LhAi9ZkPch3PP31d6KQoJ6Yc=' 'sha256-G40wI6OaLZXCtrb02xUq1H1kEVWjstzoQ0FXKwsWxPw=' 'sha256-1CldwzdEg2k1wTmf7s5RWVd7NMXI/7nxxjJM2C4DqII=' https://mixxx.discourse.group/javascripts/embed.js *.discourse-cdn.com; frame-src 'self' https://www.youtube-nocookie.com https://mixxx.discourse.group ; img-src 'self' https://i.ytimg.com ; connect-src 'self' https://mixxx.discourse.group https://*.discourse-cdn.com"
11 changes: 11 additions & 0 deletions pelicanconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,15 @@
"download_metadata",
"draft_override",
"md_yaml",
"render_math",
]

MATH_JAX = {
# Disable auto_insert because we insert our own MathJax3 <script> into the needed articles.
# This also means that any settings created here are not relayed to mathjax (see templates/article.html instead)
"auto_insert": False,
}


class MenuItem:
def __init__(self, url, title, context, css="", children=()):
Expand Down Expand Up @@ -291,6 +298,10 @@ def __init__(self, url, title, context, css="", children=()):
"email": "g73o82d65n79@protonmail.ch",
"tagline": "GSoC 2025 Contributor",
},
"Jan Claußen": {
"github": "jclsn",
"tagline": "Mixxx Contributor",
},
}

# Needed for Jinja2 markdown filter
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ markdown-full-yaml-metadata @ git+https://github.com/Holzhaus/python-markdown-fu
Markdown-Video @ git+https://github.com/Holzhaus/Python-Markdown-Video@98725ee09995fdeb05597d82defacaa7ceeca546
MarkupSafe==2.1.3
pelican==4.9.1
pelican-render-math==1.0.4
Pygments==2.17.2
python-dateutil==2.8.2
pytz==2023.3.post1
Expand Down
5 changes: 5 additions & 0 deletions theme/templates/article.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,9 @@ <h4>{% trans %}Comments{% endtrans %}</h4>
{% if article.comments and article.comments in ("true", "yes") %}
<script src="{{ SITEURL }}/theme/js/discourse.js"></script>
{% endif %}
{% if article.math and article.math in ("true", "yes") %}
{# Note that we don't insert any mathjax config. If you want to configure mathjax, see https://docs.mathjax.org/en/latest/web/configuration.html#configuring-mathjax #}
{# pinning the latest MathJax and using Subresource integrity (since that allows us to avoid whitelisting all of jsdelivr in the CSP) #}
<script id="MathJax-script" type="text/javascript" integrity="sha256-1CldwzdEg2k1wTmf7s5RWVd7NMXI/7nxxjJM2C4DqII=" crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-svg.js"></script>
{% endif %}
{% endblock %}
Loading