-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Developer Guide Engine Player
This page covers the deck and sampler playback pipeline: how audio
samples are read from disk, kept in a cache, navigated around loops and
cue points, and then rate-adjusted before being handed to EngineMixer.
The main classes are EngineBuffer, CachingReader,
ReadAheadManager, and the EngineBufferScale family.
EngineBuffer is the EngineObject that does all deck-level audio
work. It is created by EngineDeck and its process(CSAMPLE* pOut, std::size_t bufferSize) method is called once per audio callback.
EngineBuffer owns a collection of EngineControl sub-objects, each
responsible for a distinct aspect of playback:
| Control | Responsibility |
|---|---|
RateControl |
Rate slider, nudge, and scratch input; computes the final playback rate |
BpmControl |
Beat tracking: local BPM, beat distance, quantize snapping |
SyncControl |
Sync Lock participation; implements the Syncable interface |
LoopingControl |
Loop in/out points, beat loops, loop length controls |
CueControl |
Hot cues, intro/outro markers, default cue behavior |
KeyControl |
Pitch adjustment independent of tempo (key lock) |
VinylControlControl |
Vinyl timecode signal routing into the rate/scratch path |
ClockControl |
Publishes beat clock ticks for controller LED display |
Each EngineControl can register hints with the CachingReader to
keep relevant portions of the file warm in cache.
Each callback EngineBuffer::process does roughly the following:
- Consume any queued seek or sync-enable requests.
- Ask
RateControlfor the current rate (including scratch velocity). - Call
m_pReadAheadManager->getNextSamples()(via the active scaler) to fill the output buffer at the correct speed and pitch. - Deliver hints to
CachingReaderso the worker thread pre-fetches upcoming frames. - Update
VisualPlayPositionso the waveform display stays in sync. - Call
postProcessLocalBpm()/postProcess()to advance BPM and sync state after all decks have been processed.
Because file I/O is forbidden in the audio callback thread, CachingReader
splits decoding into two layers:
-
CachingReader(callback thread) — exposesread()andhintAndMaybeWake(). Maintains an LRU cache of decoded chunks. -
CachingReaderWorker(background QThread) — woken byCachingReaderwhen cache misses are detected. Calls theSoundSourceAPI to decode audio and writes chunks back via a wait-free FIFO.
A chunk is a fixed-size contiguous block of decoded CSAMPLE frames.
Chunks are stored in a flat array and tracked in a doubly-linked LRU
list. When a chunk is accessed (read or hinted) it moves to the back of
the list. When a new chunk needs to be allocated and the cache is full,
the front (least-recently-used) chunk is recycled.
enum class ReadResult {
UNAVAILABLE, // cache miss; buffer untouched
PARTIALLY_AVAILABLE, // some frames missing; gaps filled with silence
AVAILABLE, // all requested frames present
};
ReadResult read(SINT startSample, SINT numSamples, bool reverse,
CSAMPLE* buffer,
mixxx::audio::ChannelCount channelCount);UNAVAILABLE means the caller should try again next callback; the
worker has been woken to fetch the missing chunk. PARTIALLY_AVAILABLE
means a loop boundary was crossed and the non-missing portion was
returned.
A Hint tells CachingReader which frames will be needed soon —
beyond just the current read position. Every EngineControl can add
hints to a HintVector that is passed to hintAndMaybeWake() at the
end of each callback. Hint types include CurrentPosition,
LoopStartEnabled, MainCue, HotCue, IntroStart, etc. The hint
system ensures that common jump targets (hot cues, loop points) are
already decoded before the user activates them.
typedef struct Hint {
SINT frame; // start of the region to warm
SINT frameCount; // kFrameCountForward (0) or kFrameCountBackward (-1)
// for default forward/backward prefetch
Type type;
} Hint;
void hintAndMaybeWake(const HintVector& hintList);ReadAheadManager sits between CachingReader and
EngineBufferScale. Its job is to serve consecutive blocks of raw
(non-rate-adjusted) frames while correctly handling:
-
Forward and reverse playback — passes
reverse=truetoCachingReader::read(). -
Loop boundaries — consults
LoopingControlbefore reading to detect when the read head would cross a loop out-point, and wraps back to the loop in-point seamlessly. -
Quantized cue triggers — consults
CueControlto detect whether a cue point should trigger a seek mid-buffer.
SINT getNextSamples(double dRate, CSAMPLE* buffer,
SINT requested_samples,
mixxx::audio::ChannelCount channelCount);The dRate sign determines direction. The return value is the number of
samples actually written, which may be less than requested_samples if
a boundary was hit — the scaler must call getNextSamples again to
fill the remainder.
ReadAheadManager maintains a small internal log of (start, end)
pairs for each getNextSamples call. After the scaler finishes
consuming samples, getFilePlaypositionFromLog() uses this log to
translate the number of scaler output frames consumed back into the
exact file frame position, even when the rate changed mid-buffer.
EngineBufferScale is the abstract base class for all rate and pitch
adjustment implementations. EngineBuffer holds two instances: one for
vinyl/scratch (always linear) and one for keylock mode (SoundTouch or
RubberBand).
The user-selectable keylock algorithm is encoded in KeylockEngine:
enum class KeylockEngine {
SoundTouch = 0,
RubberBandFaster = 1, // __RUBBERBAND__ only
RubberBandFiner = 2, // __RUBBERBAND__ only, requires RubberBand R3
};The active EngineBufferScale implementation is hot-swapped at
runtime when the user changes the keylock engine setting.
class EngineBufferScale : public QObject {
public:
// Set rate and pitch parameters before each call to scaleBuffer.
// base_rate = track sample rate / output sample rate
// tempoRatio = ratio of track time to real time (1.0 = normal speed)
// pitchRatio = pitch multiplier (1.0 = no pitch change)
virtual void setScaleParameters(double base_rate,
double* pTempoRatio,
double* pPitchRatio);
// Reset internal buffers after a seek.
virtual void clear() = 0;
// Fill pOutputBuffer with iOutputBufferSize samples.
// Returns the number of *input* frames consumed (may be fractional).
virtual double scaleBuffer(CSAMPLE* pOutputBuffer,
SINT iOutputBufferSize) = 0;
};src/engine/bufferscalers/enginebufferscalelinear.h
Used for vinyl/scratch and when keylock is off. Performs simple linear interpolation between adjacent frames. Very low CPU cost, no latency, but does not preserve pitch when tempo changes.
src/engine/bufferscalers/enginebufferscalest.h
Wraps the SoundTouch library. Offers independent tempo and pitch processing via WSOLA (Waveform Similarity Overlap-Add). Lower CPU usage than RubberBand; always available regardless of build configuration.
src/engine/bufferscalers/enginebufferscalerubberband.h
Wraps the RubberBand library.
Available when Mixxx is compiled with __RUBBERBAND__. Two quality
modes are exposed:
-
RubberBandFaster (
RubberBandFaster = 1) — uses RubberBand's faster R2 engine; good quality at moderate CPU cost. -
RubberBandFiner (
RubberBandFiner = 2) — uses RubberBand's R3 engine (near-hi-fi quality); highest CPU cost; only available if the installed RubberBand version supports it.
RubberBand processing is driven via RubberBandWorkerPool, a pool of
EngineWorker threads that run DSP work between callbacks to avoid
doing it all inline in the callback thread.
Seeks are not applied immediately. Instead they are queued atomically and
consumed at the start of the next callback. The SeekRequest flags
control how the target position is adjusted:
enum SeekRequest {
SEEK_NONE = 0,
SEEK_PHASE = 1 << 0, // snap to nearest beat (quantize)
SEEK_EXACT = 1 << 1, // seek to exact frame, bypass quantize
SEEK_EXACT_PHASE = SEEK_PHASE | SEEK_EXACT,
SEEK_STANDARD = 1 << 2, // SEEK_PHASE if quantize on, else SEEK_EXACT
SEEK_CLONE = 1 << 3, // jump to the current position of another deck
};queueNewPlaypos(FramePos, SeekRequest) is the external entry point.
When multiple seek requests arrive in the same callback they are merged
using bitwise OR.
After a seek is consumed, EngineBufferScale::clear() is called on
the active scaler to flush its internal buffers and avoid any
discontinuity-induced artefacts from pre-seek audio data.
Slip mode maintains a parallel "slip position" that advances at normal
1× speed while scratching or looping is active. When slip mode is
disabled the engine jumps to the accumulated slip position, giving the
impression that playback continued uninterrupted underneath the
manipulation. processSlip() updates the slip position each callback.
Mixxx is a free and open-source DJ software.
Manual
Hardware Compatibility
Reporting Bugs
Getting Involved
Contribution Guidelines
Coding Guidelines
Using Git
Developer Guide
Creating Skins
Contributing Mappings
Mixxx Controls
MIDI Scripting
Components JS
HID Scripting