Skip to content

Instantly share code, notes, and snippets.

@mxmilkiib
Last active October 21, 2025 13:38
Show Gist options
  • Select an option

  • Save mxmilkiib/ad55a02d3aea12ecaa8bc6acdc3484e5 to your computer and use it in GitHub Desktop.

Select an option

Save mxmilkiib/ad55a02d3aea12ecaa8bc6acdc3484e5 to your computer and use it in GitHub Desktop.
MIXXX_ARCHITECTURE_GUIDE.md, made by Claude Sonnet 4.5 thinking - WIP!!!

Mixxx Architecture Guide

A guide to understanding Mixxx's programming patterns, frameworks, and architectural decisions.

WIP!!!

Target: Mixxx main branch (development) — This document reflects the current development state and may include features not yet in stable releases.


Table of Contents

  1. External Documentation

  2. System Overview

    • Architecture at a Glance
      • Core Paradigm, Design Principles, Three Fundamental Threads, Qt Framework Integration, Manager Organization, ControlObject Communication, Data Flows
    • Performance Characteristics
      • Real-Time Audio, Playback and Track Handling, Library and Database, Waveform Rendering, Effects Processing, Memory Footprint, Analysis Performance, Threading Model, Scaling Characteristics, CPU Distribution
    • Core Libraries
      • Qt Framework, Audio Infrastructure, Track Analysis, Supporting Infrastructure
    • Code Organization
      • Directory Structure, Namespace Organization, Finding Existing Code
    • Build System
      • CMake Configuration, Feature Flags, Dependencies and vcpkg, Compilation Process, Platform-Specific Details
    • Configuration Files
      • User Data Directory, mixxx.cfg, mixxxdb.sqlite, Controllers, Skins, Effects, Analysis, Logs
    • Application Lifecycle
      • Startup Sequence, Shutdown, Typical User Interaction Flow
    • Qt Integration Patterns
      • Qt Meta-Object System
      • Signal/Slot Connection Patterns
      • Value Change Request Pattern
      • Qt Smart Pointers
    • Key Architectural Patterns
      • Dependency Injection, Factory, Singleton, RAII, Observer, Bridge, Proxy, Command, DAO, Facade, Adapter, Strategy
    • Coding Style Essentials
    • Extension Points
  3. Major Subsystems

    • Manager Pattern
      • Core Services Architecture
      • Manager Responsibilities
      • Lifecycle and Initialization
      • Ownership Model
      • Dependency Injection Pattern
      • Manager Communication Patterns
      • Complete Example: Loading a Track
      • Thread Safety Considerations
      • Testing with Mock Managers
    • Engine
      • EngineMixer
      • EngineBuffer
      • EngineControl subclasses (CueControl, LoopingControl, BpmControl, RateControl)
      • EngineChannel
      • CachingReader
      • Subsystem Integration (Real-Time Processing, Library, Controls, Effects, User Input, Sync)
    • Control System
      • ConfigKey (Universal Addressing)
      • ControlObject
      • ControlProxy
      • ControlDoublePrivate
      • Specialized Types (ControlPushButton, ControlPotmeter, ControlIndicator)
      • ControlBehavior (Linear, Logarithmic, Potmeter)
      • Subsystem Integration (Engine, UI Binding, Controller Scripting, Effects, Persistence)
    • Library
      • TrackCollection
      • DAOs (TrackDAO, CueDAO, PlaylistDAO)
      • TrackPointer (QSharedPointer)
      • LibraryScanner
      • GlobalTrackCache
      • Subsystem Integration (Data Management, Threading, Engine, UI Updates, Analysis)
    • Controllers
      • ControllerManager
      • MIDI/HID Controllers
      • XML Mapping
      • QJSEngine (JavaScript Scripting)
      • Bidirectional Communication (Input/Output, LED Feedback)
      • Subsystem Integration (Hardware, Scripting, Library Access, Engine Control, Effects)
    • Effects
      • EffectsManager
      • EffectChain (4 chains)
      • EffectSlot (4 slots per chain)
      • EffectProcessor (Built-in, LV2 plugins)
      • Routing (Send/Return, Insert modes)
      • Subsystem Integration (Audio Processing, Routing, Control Exposure, Mixer, Tempo Sync)
    • Mixer
      • PlayerManager (Factory Pattern)
      • Deck (4 decks)
      • Sampler (64 samplers)
      • Microphone/Auxiliary
      • Subsystem Integration (Player Lifecycle, Control Exposure, Library, Effects Routing, Mixing Algorithm)
    • Skin/UI
      • LegacySkinParser
      • WBaseWidget (Widget Hierarchy)
      • Declarative XML Binding
      • Qt Stylesheets (QSS)
      • QML Integration
      • Subsystem Integration (Control Binding, Waveform Rendering, Library Models, User Input)
  4. Engine and Audio Processing Details

  5. Audio File Formats and Extensions

    • Stems
      • Multi-Track Playback, Stem Routing, Isolation Controls
    • Module Files
      • Tracker Format Support, libopenmpt Integration
    • Plugin System
      • LV2 Plugin Loading, Effect Backend Architecture
  6. Effects System Details

  7. Track Analysis Pipeline

  8. Library and Database Architecture

  9. Widget and UI Patterns

  10. Waveform Rendering

  11. Preferences and Settings

  12. Controllers and Scripting Details

  13. Special Features

  14. Integration Examples

  15. Debugging and Troubleshooting

  16. Testing Infrastructure

  17. Performance Optimization

  18. Migration and Version Notes

  19. Anti-Patterns


External Documentation

Mixxx Documentation

Qt Documentation

C++ References

QSS/QML Resources

Learn X in Y minutes

Community


System Overview

Architecture at a Glance

Core Paradigm (Control System, Threading Model):

  • Control-oriented, multi-threaded Qt application with real-time audio processing at ~5-20ms latency (Real-Time Audio Thread Requirements)
  • Thread-safe communication layer connecting every subsystem via ControlObject (Control System)
    • The secret sauce: Controls (ConfigKey) are the glue between all subsystems
    • Thread safety: Controls use atomic operations (QAtomicInteger)—safe to read from any thread
    • Example: Audio thread (Audio Thread) sets position, GUI thread (GUI Thread) reads it to update waveform
  • Fundamental insight: think "what controls expose this functionality?" not "what objects implement this?"
  • Observer Pattern (Observer Pattern) where everything communicates through the Control System using ConfigKey addressing

Design Principles (Key Architectural Patterns, Memory Management):

Three Fundamental Threads (plus optional worker pools, see Threading Model for complete details):

  1. Main/GUI Thread: Qt event loop for UI updates and user interaction (GUI Thread)
  2. Audio Thread: Real-time audio processing at ~5-20ms latency using lock-free algorithms (Audio Thread, Real-Time Audio Thread Requirements)
  3. Library Thread: Database I/O, track scanning, metadata operations (Library Thread, LibraryScanner)
  4. Worker Thread Pools (main branch, optional, see Threading Model):

Qt Framework Integration (Qt Integration Patterns, Core Libraries):

Mixxx is built on the Qt framework (Qt6 default since 2.5.0, Qt6 Migration), which provides the foundational infrastructure for the entire application. Qt's signals and slots mechanism is the glue connecting subsystems:

How signals/slots work (Signal/Slot Connection Patterns, Observer Pattern):

  • Signal: Event notification ("play button was clicked")
  • Slot: Event handler (function that responds to signal)
  • Connection: Links signal to slot—when signal emitted, slot is called
  • Magic: Qt automatically routes across thread boundaries safely (Threading Model)
    • Same thread (Qt::DirectConnection): Slot called immediately (direct function call)
    • Cross-thread (Qt::QueuedConnection): Message queued, slot called later on target thread (Three Fundamental Threads)
    • Auto (Qt::AutoConnection): Qt chooses based on thread affinity
  • Example: Control changes in audio thread (Audio Thread) → signal emitted → GUI widget updates (queued to GUI Thread)

Qt Meta-Object System (MOC) (Build System, Qt Meta-Object System):

  • MOC = Meta-Object Compiler—preprocessor that runs before C++ compiler
  • Processes: Classes with Q_OBJECT macro (CMake Configuration)
  • Generates: Meta-information for signals/slots, runtime type info, properties
  • Build step: CMake runs moc on headers → generates moc_*.cpp files → compiled into app (AUTOMOC setting)
  • Why needed: C++ doesn't have reflection—MOC adds it

Qt's object tree automatically manages memory (Memory Management, Qt Smart Pointers):

  • Parent-child relationship: Every QObject can have a parent
  • Rule: When parent deleted, all children auto-deleted recursively (RAII)
  • Example: Deck (Mixer) deleted → all EngineControls (Engine) auto-deleted
  • Benefit: Eliminates manual delete calls—no memory leaks
  • In code: new ControlObject(group, item, this)this is parent (Creating Controls)
  • Why parented_ptr<T>: Enforces parent ownership for Qt objects (prevents accidental std::unique_ptr on QObject)

Qt event loop drives the GUI thread (GUI Thread, Application Lifecycle):

  • Job: Dispatches events (mouse, keyboard, timers, queued signals)
  • Loop: while(true) { wait for event; process event; update UI; }
  • Responsiveness: If event handler takes 100ms, UI freezes—keep it fast! (Performance Characteristics)

Thread affinity (Threading Model, Thread Safety Considerations):

  • Rule: Qt objects "live" on the thread where they were created
  • Consequence: Can't call QObject methods from different thread (will crash or corrupt)
  • Exception: A few methods are thread-safe (explicitly documented)
  • Cross-thread signals: Automatically queued—target slot runs on object's thread
  • Why this matters: ControlObject (Control System) uses QAtomicInteger (not QObject methods) so audio thread can read safely (Real-Time Audio Thread Requirements)

Manager Organization (one manager per major subsystem):

  • Manager objects follow dependency injection pattern via CoreServices
    • Dependency injection: Objects receive their dependencies as constructor arguments
    • vs. Singletons: Managers passed explicitly, not accessed via global getInstance()
    • Benefit: Testable—can pass mock managers in unit tests
  • CoreServices (.cpp) instantiates and wires major managers:
    • The root: Created at startup, owns all managers, coordinates initialization order
    • Startup sequence: SoundManager → PlayerManager → EffectsManager → Library → Controllers
    • Shutdown sequence: Reverse order—controllers → library → effects → players → sound
    • Why order matters: PlayerManager needs SoundManager to exist first

The Managers (each owns one subsystem, see Manager Pattern for full details):

ControlObject Communication System (Control System):

Data Flows (Performance Characteristics, Complete Example: Loading a Track):

Performance Characteristics

Real-Time Audio (Real-Time Audio Thread Requirements):

  • Latency: 5-20ms (typical 256-1024 sample buffers @ 48kHz) - configurable in SoundManager preferences
  • Audio thread priority: SCHED_FIFO (Linux), High priority (Windows/macOS) for Audio Thread
  • Buffer processing deadline: ~21ms @ 1024 samples, 48kHz - must complete EngineBuffer::process() within this time
  • Glitch-free requirement: All processing must complete within buffer time (no allocations, no locks, no I/O)

Playback and Track Handling (Engine):

Library and Database (Library):

  • Library query (sorted/filtered): <10ms for <50k tracks, SQLite B-tree indexes via DAO Pattern
  • Track scanner: ~10-50 tracks/second (disk-bound, metadata parsing by LibraryScanner)
  • Database size: ~5-10MB per 10k tracks in mixxxdb.sqlite (Configuration Files)
  • Startup library load: ~100-500ms for 10k tracks (Application Lifecycle)

Waveform Rendering (Waveform Rendering):

  • OpenGL: 60fps, ~5-10% CPU per waveform, 50-100MB VRAM (via QtOpenGL)
  • Software (QPainter): 30fps, ~15-25% CPU per waveform (fallback when GPU unavailable)
  • Waveform zoom/scroll: <16ms per frame (60fps target for smooth Skin/UI)

Effects Processing (Effects System):

  • Per effect overhead: 1-10% CPU depending on complexity (Built-In Effects vs LV2 plugins)
  • Effect chain latency: ~1 buffer (5-20ms additional) per EffectChain
  • LV2 plugin loading: ~50-200ms per plugin (Plugin System)

Memory Footprint (Memory Management, Performance Characteristics):

  • Base application: ~100-200MB breakdown:
    • Qt libraries: ~40-60MB (Core Libraries: QtCore, QtWidgets, QtSql, QtOpenGL shared libraries)
    • Audio codecs: ~20-30MB (Audio Infrastructure: libFLAC, libvorbis, libmp3lame, SoundTouch, RubberBand)
    • Managers and controllers: ~15-25MB (Manager Pattern: PlayerManager, EffectsManager, ControllerManager state)
    • Control system registry: ~5-10MB (Control System: 1000+ ControlObjects, hash table overhead)
    • Database connections: ~5-10MB (Library: SQLite cache, prepared statements)
    • Skin assets: ~20-40MB (Skin/UI: fonts, images, textures loaded at startup)
  • Per loaded track: ~2-10MB breakdown (Track Pointer Pattern):
    • Waveform summary: ~1-2MB (Waveform Generation: compressed RGB data, 1024-2048 points per minute)
    • Beat grid: ~50-100KB (BPM Detection: FramePos array for beat markers)
    • Metadata cache: ~10-50KB (QString for artist, title, album, genre, comment)
    • Cue points: ~1-10KB (CueDAO: up to 36 hotcues, 5 main cues per track)
    • Track object overhead: ~100-200KB (QObject, signals, internal state)
  • Large library (50k tracks): ~300-500MB memory (Library and Database Architecture):
    • GlobalTrackCache: ~250-400MB (Library: weak pointers + loaded Track objects)
    • SQLite cache: ~30-60MB (DAO Pattern: query cache, B-tree index pages in RAM)
    • Model data: ~20-40MB (Library Models: QAbstractItemModel internal storage for visible rows)
  • Waveform rendering buffers: ~50-100MB per visible waveform (Waveform Rendering):
    • GPU textures (OpenGL): ~50-80MB (waveform data uploaded to VRAM)
    • Software rendering (QPainter): ~20-40MB (QImage buffers in system RAM)
  • Audio buffers: ~5-10MB total (Engine):
    • CachingReader ring buffers: ~2-5MB per deck (Audio Buffer Processing: 1-5 seconds pre-buffered)
    • EngineMaster mix buffers: ~100KB (temporary processing buffers)
    • SoundManager I/O buffers: ~50KB (Audio Infrastructure: PortAudio/JACK/ALSA driver buffers)
  • Controller scripts: ~5-15MB (Controllers):
    • QJSEngine: ~3-5MB per controller (JavaScript Scripting: JavaScript VM overhead)
    • Script bytecode: ~1-3MB (compiled controller mapping scripts)
  • Effects: ~10-30MB total (Effects System):
    • LV2 plugins: ~5-10MB per plugin (LV2 Backend: DSP state, internal buffers)
    • Built-in effects: ~2-5MB per effect (pre-allocated processing buffers)
    • Effect chains: ~1MB per chain (EffectChain: routing matrix, parameter state)

Analysis Performance (Track Analysis Pipeline, background thread, per track):

  • BPM detection: 0.3-1x realtime (BPM Detection, Queen Mary DSP)
  • Key detection: 0.5-2x realtime (Key Detection, KeyFinder algorithm)
  • Waveform generation: 2-5x realtime (Waveform Generation)
  • ReplayGain: 3-8x realtime (ReplayGain Analysis, libebur128)
  • Parallel: 4 tracks simultaneously on quad-core (thread pool, N-1 cores)

Threading Model (Three Fundamental Threads):

Scaling characteristics (performance vs feature count):

  • Deck scaling: ~2x CPU for audio processing
    • 1 deck: ~5% CPU (baseline: playback + EQ + one waveform)
    • 2 decks: ~10% CPU (typical DJ setup)
    • 4 decks: ~20% CPU (professional setup, crossfader between pairs)
    • Reason: Linear scaling because each deck processes independently
  • Stems playback: +10-15% CPU per stem deck
    • Regular stereo: 1 audio stream (2 channels)
    • 4-stem: 4 audio streams (8 channels total)
    • Overhead: Decoding 4 streams + stem mixing + per-stem effects routing
    • Example: 2 regular decks (10%) + 2 stem decks (30%) = ~40% total CPU
  • Effects per chain: Linear CPU scaling
    • 0 effects: Baseline (~10% for 2 decks)
    • 1 simple effect (tremolo): +1-2% CPU
    • 1 complex effect (reverb): +5-10% CPU
    • 4-effect chain: +15-30% CPU (depending on complexity)
    • Reason: Effects process serially in chain (output₁ → input₂ → output₂ → input₃...)
  • Waveforms displayed: Linear GPU/CPU scaling
    • 0 waveforms: Baseline (~10% for 2 decks)
    • 2 waveforms (OpenGL): +10-16% CPU, ~100-200MB VRAM
    • 4 waveforms (OpenGL): +20-32% CPU, ~200-400MB VRAM
    • 2 waveforms (software): +30-50% CPU (QPainter rendering)
    • Reason: Each waveform is independent widget with own render loop
  • Samplers: +1-2% CPU per active sampler
    • Inactive samplers: <0.1% CPU (just control polling)
    • Playing sampler: ~1-2% CPU (audio playback, no EQ by default)
    • 64 samplers all playing: +64-128% CPU (impractical, but technically possible)
  • Library size: Sublinear scaling (O(log n) for queries)
    • 1,000 tracks: ~10ms for sorted query (B-tree index depth: ~2-3)
    • 10,000 tracks: ~15ms for sorted query (B-tree index depth: ~3-4)
    • 100,000 tracks: ~25ms for sorted query (B-tree index depth: ~4-5)
    • Reason: SQLite B-tree indexes provide logarithmic lookup time
  • Controllers connected: +2-5% CPU per controller
    • MIDI polling: ~1ms every 1-5ms (USB polling rate)
    • JavaScript callbacks: ~10-50µs per MIDI message
    • LED updates: ~100-500µs per LED state change (MIDI output)
    • Example: 3 controllers = ~6-15% CPU overhead

Typical CPU distribution (4-deck mixing):

For detailed performance breakdowns and optimization strategies, see Per-Feature Performance Costs and Performance Optimization.

Core Libraries

Qt Framework (Qt6, Qt6 default since 2.5.0):

Audio Infrastructure (Engine and Audio Processing):

Track Analysis (Track Analysis Pipeline, background processing on Analyzer Pool):

  • chromaprint: Audio fingerprinting for AcoustID track identification
    • Algorithm: Chroma-based fingerprinting with spectral analysis
    • Output: Compact fingerprint hash for online track lookup
    • Use case: Identify unknown tracks, find duplicates
    • See also: Track Analysis Pipeline, Library
  • libebur128: ReplayGain loudness normalization (ReplayGain Analysis)
    • Standard: EBU R128 / ITU-R BS.1770 for broadcast loudness
    • Measurement: Integrated loudness (LUFS), loudness range (LU)
    • Application: Automatic volume normalization for consistent playback levels
    • Performance: ~3-8x realtime (faster than playback)
    • See also: Analysis Performance, Track Analysis Pipeline
  • KeyFinder: Musical key detection (Key Detection)
    • Algorithm: Krumhansl-Schmuckler key-finding algorithm
    • Output: Musical key (e.g., "6d" for A minor) using Lancelot/Camelot notation
    • Use case: Harmonic mixing, key-compatible track selection
    • Performance: ~0.5-2x realtime
    • See also: Key Detection, Harmonic Mixing
  • Queen Mary DSP: Beat detection and BPM analysis (BPM Detection)
    • Algorithm: Onset detection + autocorrelation for tempo estimation
    • Output: Beat grid with FramePos markers, BPM value
    • Accuracy: Typically within 0.1 BPM for electronic music
    • Performance: ~0.3-1x realtime (fastest analyzer)
    • See also: BPM Detection, Beat Grid, Sync System

Supporting Infrastructure:

  • Protocol Buffers: Binary serialization for efficient data storage and interchange
    • Beat grid storage: Serializes beat markers and BPM metadata to mixxxdb.sqlite (Database Schema)
    • Library export/import: Enables portable library backups with version-independent schema evolution
    • Schema versioning: Forward/backward compatibility when adding new track metadata fields
    • Performance: Compact binary format reduces database size and I/O overhead vs. XML/JSON
    • Type safety: Generated C++ classes provide compile-time validation of data structures
    • Used in: Track Analysis Pipeline for storing waveform summaries, beat grids, and key signatures
  • hidapi: HID controller support for USB devices (DJ controllers, MIDI controllers with HID mode) in ControllerManager
  • portmidi / RtMidi: MIDI I/O for hardware integration in ControllerManager (MIDI Message Flow)
  • SQLite: Embedded database at ~/.mixxx/mixxxdb.sqlite for track metadata, cues, playlists (DAO Pattern, Database Schema)
  • taglib: ID3/metadata reading from audio files (MP3, FLAC, MP4, OGG) during track scanning by LibraryScanner

Code Organization

Directory Structure:

Top-Level Layout:

/src/               # Source code
/res/               # Resources (images, fonts, controller mappings)
/build/             # Out-of-source build directory
/lib/               # Third-party libraries (Windows/macOS)
/cmake/             # CMake modules

Browse the repository structure on GitHub: mixxx repository root

Source Code Organization (/src/):

src/
	control/          # control system core (ControlObject, ControlProxy)
	engine/           # audio processing engine
		controls/       # deck features (cue, loop, rate, clock)
		channels/       # channel types (deck, sampler, aux, microphone)
	library/          # database, tracks, playlists
		dao/            # data access objects
	controllers/      # MIDI/HID hardware integration
		scripting/      # JavaScript engine integration
	mixer/            # player management (PlayerManager)
	effects/          # effects system (chains, processors)
	widget/           # UI widgets (WPushButton, WLabel, etc.)
	skin/             # skin parsing and rendering
	track/            # track metadata, cues, beats, keys
	util/             # utility classes (types, assertions, colors)
	test/             # test infrastructure (MixxxTest, etc.)
	preferences/      # user settings and configuration
	soundio/          # audio I/O (SoundManager)
	waveform/         # waveform rendering

Key directories: control/ contains the control system core (ControlObject, ControlProxy). engine/ houses audio processing with controls/ subdirectory for deck features (CueControl, LoopingControl, BpmControl, RateControl, ClockControl). library/ manages database and tracks with dao/ for data access objects (TrackDAO, CueDAO, PlaylistDAO). controllers/ handles MIDI/HID integration with scripting/ for JavaScript engine. mixer/ contains PlayerManager. effects/ implements effects chains and processors. widget/ and skin/ handle UI rendering. test/ contains test infrastructure. util/ provides utility classes and type definitions.

Namespace Organization (The mixxx Namespace):

Mixxx uses C++ namespaces for modular code organization:

namespace mixxx {
		// Core types and utilities (*[Types and Namespaces](#types-and-namespaces)*)
		namespace audio { /* ChannelCount, SampleRate, SignalInfo - *[Audio Type Conventions](#audio-type-conventions)* */ }
		namespace qml { /* QML/Qt Quick integration */ }
		namespace network { /* HTTP requests, web services */ }
		namespace skin { /* Skin parsing and rendering - *[Skin System](#skinui-srcskin-srcwidget)* */ }
		namespace library { /* Track collection, playlists - *[Library](#library-srclibrary)* */ }
}

Benefits:

  • Prevents naming conflicts: mixxx::audio::FramePos vs global FramePos (Strongly-Typed IDs)
  • Logical grouping: Related functionality bundled together
  • Cleaner includes: using namespace mixxx::audio; for local scope
  • Forward compatibility: Easy to add new modules without breaking existing code

Finding Existing Code:

# To find control definitions (*[ConfigKey](#configkey-universal-addressing)*)
grep -r "ConfigKey.*hotcue" src/engine/controls/
# or use ripgrep for faster searching
rg "ConfigKey.*hotcue" src/engine/controls/

# To find signal/slot connections (*[Signal/Slot Connection Patterns](#signalslot-connection-patterns)*)
grep -r "connect.*valueChanged" src/

# To find EngineControl subclasses (*[EngineControl](#engine-srcengine)*)
grep -r "class.*EngineControl" src/engine/

Build System

CMake (3.21+ required, 3.28+ recommended):

  • Out-of-source builds required (build artifacts in /build/ directory):
    • Reason: Keeps source tree clean, allows multiple build configurations
    • Pattern: cmake -B build -S . (build dir: build/, source dir: .)
    • Multiple configs: cmake -B build-debug -DCMAKE_BUILD_TYPE=Debug + cmake -B build-release -DCMAKE_BUILD_TYPE=Release
    • Gitignore: /build*/ excludes all build directories from version control
  • AUTOMOC for Qt meta-object compilation:
    • Automatic: Scans all .h headers for Q_OBJECT macro
    • Generates: build/src/mixxx_autogen/moc_*.cpp files (~200 files for Mixxx)
    • Timing: MOC generation ~5 seconds for full build, <1 second incremental
    • Manual override: qt_wrap_cpp() for headers with special requirements
    • Include paths: #include "moc_classname.cpp" at end of .cpp file (unity build optimization)
  • vcpkg for Windows/macOS dependencies:
    • Manifest mode: vcpkg.json declares all dependencies with versions
    • Automatic install: CMake toolchain file triggers dependency install before configure
    • Binary caching: Reuses pre-built dependencies across machines (~10x faster)
    • Triplets: x64-windows, x64-osx, arm64-osx for platform-specific builds
    • Custom ports: Mixxx-specific patches in build/vcpkg-ports/ for specialized builds
  • System packages on Linux:
    • Debian/Ubuntu: apt install (~30 packages: Qt6, PortAudio, codecs, analysis libraries)
    • Fedora: dnf install (equivalent package names)
    • Arch: pacman -S (bleeding-edge versions)
    • Development headers: Requires -dev or -devel suffixed packages
  • Feature flags (configure with cmake -D<FLAG>=ON/OFF):
    • QML=ON: Enable Qt Quick/QML UI support (~10MB additional binary size)
    • QOPENGL=ON: GPU-accelerated waveform rendering (default: ON)
    • FFMPEG=ON: Additional codec support via FFmpeg (AAC, WMA, etc.)
    • MODPLUG=ON: Module file support (MOD, XM, IT, S3M tracker formats)
    • LILV=ON: LV2 plugin host support (default: ON)
    • QTKEYCHAIN=ON: Secure credential storage for broadcast passwords
    • BULK=ON: USB bulk controller support (Denon DN-S3700, etc.)
    • HID=ON: HID controller support (most modern DJ controllers)
    • Feature detection: find_package() auto-detects available libraries
  • Build types (CMAKE_BUILD_TYPE):
    • Debug: -O0 -g (no optimization, full debug symbols, ~80MB binary)
      • Use case: Development, debugger stepping, variable inspection
      • Performance: ~2-5x slower than Release
      • Assertions: All DEBUG_ASSERT() active (crash on violations)
    • Release: -O3 -DNDEBUG (maximum optimization, no debug info, ~15MB binary)
      • Use case: Production builds, performance testing, distribution
      • Performance: Fastest execution, ~20-30% faster than RelWithDebInfo
      • Assertions: All DEBUG_ASSERT() compiled out (silent failures)
      • Inlining: Aggressive function inlining, harder to profile
    • RelWithDebInfo: -O2 -g -DNDEBUG (optimization + symbols, ~40MB binary)
      • Use case: Default for development, profiling, crash debugging
      • Performance: Nearly as fast as Release (~5-10% slower)
      • Debugging: Can set breakpoints, inspect optimized variables (may be inaccurate)
      • Profiling: Best for perf/Instruments (symbols available, optimized code paths)
    • MinSizeRel: -Os -DNDEBUG (optimize for size, ~12MB binary)
      • Use case: Embedded systems, distribution size constraints
      • Trade-off: Slightly slower than Release (~10-15%) but smaller binary
  • Compilation database: CMAKE_EXPORT_COMPILE_COMMANDS=ON generates build/compile_commands.json
    • Used by: clangd, cppcheck, clang-tidy, VS Code, CLion
    • Contents: Full compiler invocation for every source file
    • Symlink: ln -s build/compile_commands.json . makes tools auto-discover
  • Parallel builds: cmake --build build --parallel N (N = CPU core count)
    • Speedup: 8 cores ~6-8x faster than single-threaded (Amdahl's law)
    • Bottleneck: Linking is serial (can't parallelize final executable link)
    • RAM usage: ~500MB-1GB per parallel job (total: N × 1GB for large builds)
  • Incremental build timing:
    • Change .cpp file: ~2-5 seconds (recompile 1 file, relink)
    • Change .h file: ~10-60 seconds (recompile all includers, relink)
    • Change Q_OBJECT header: +5 seconds (regenerate MOC file)
    • Add new source file: ~30 seconds (CMake reconfigure, compile new file, relink)
    • Full clean build: ~2-10 minutes (depends on CPU cores, ccache hit rate)

Configuration Files

User Data (~/.mixxx/ on Linux/macOS, %LOCALAPPDATA%\Mixxx\ on Windows, Preferences and Settings):

  • mixxx.cfg: User settings (XML format, ~50-500KB, UserSettings):
    • Structure: Nested <group> elements with <item> key-value pairs (ConfigKey)
    • Persistence: Controls with bPersist=true auto-save on application shutdown (Control System)
    • Write strategy: Atomic write to temp file + rename (prevents corruption on crash)
    • Example section:
       <group name="[Channel1]">
       	<item name="rateRange" value="0.08"/>  <!-- ±8% pitch range -->
       	<item name="quantize" value="1"/>     <!-- Quantize enabled -->
       	<item name="replaygain" value="1"/>   <!-- ReplayGain enabled -->
       </group>
       <group name="[Master]">
       	<item name="balance" value="0"/>      <!-- Center balance -->
       	<item name="headGain" value="1"/>     <!-- Headphone gain 1.0 -->
       </group>
    • Migration: UserSettings::upgrade() updates config when Mixxx version changes (Config Versioning)
    • Defaults: Missing items use hardcoded defaults from ControlObject constructors (Creating Controls)
    • Thread safety: Read/write protected by QReadWriteLock (multiple readers, single writer, Thread Safety Considerations)
  • mixxxdb.sqlite: Track library database (SQLite, ~5-10MB per 10k tracks, Library and Database Architecture):
    • Schema version: Tracked in library table, migrated via numbered SQL scripts (DAO Pattern)
    • Tables: library (tracks), track_locations (file paths), cues (hotcues/loops), playlists, playlist_tracks, crate_tracks, LibraryHashes (file change detection) — see Database Schema
    • Indexes: B-tree on artist, title, bpm, key, datetime_added for fast sorting (TrackDAO)
    • Full-text search: FTS5 virtual table on artist/title/album for instant search (Library)
    • Triggers: Auto-update datetime_modified on row changes
    • Transactions: All multi-row operations wrapped in BEGIN/COMMIT for atomicity
    • Pragma settings: journal_mode=WAL (write-ahead log for concurrency), synchronous=NORMAL (balanced durability/performance)
    • Vacuum: Manual VACUUM command reclaims space after bulk deletions (~10-50% size reduction)
    • Backup: Copy .sqlite + .sqlite-shm + .sqlite-wal files (while Mixxx closed)
  • controllers/: User controller mappings directory (Controllers):
    • Structure: One subdirectory per controller brand/model
    • Files per controller:
      • <controller>.midi.xml: Input/output MIDI mappings (hardware → control bindings, MIDI Controller Mapping)
      • <controller>-scripts.js: JavaScript initialization and callback functions (MIDI Scripting, QJSEngine)
      • <controller>.hid.xml: HID mappings (for controllers without MIDI mode, HID Support)
    • XML schema: <MixxxControllerPreset> with <controller> info + <controls> mappings
    • Example mapping:
       <control>
       	<status>0x90</status>        <!-- Note On -->
       	<midino>0x10</midino>        <!-- Note 16 (play button) -->
       	<group>[Channel1]</group>
       	<key>play</key>              <!-- Maps to [Channel1],play control -->
       </control>
    • Hot reload: Controllers can be disabled/re-enabled to reload scripts (no restart, ControllerManager)
  • skins/: User skins directory (Skin/UI):
    • Structure: One subdirectory per skin name
    • Required files: skin.xml (main layout, LegacySkinParser), style.qss (Qt stylesheet, QSS Documentation)
    • Assets: Images (PNG/SVG), fonts (TTF/OTF), templates (reusable XML fragments)
    • Resolution: Mixxx looks in user dir first, then system skins (/usr/share/mixxx/skins/, Creating Skins)
  • effects.xml: Effects configuration (~5-20KB, Effects System):
    • Stores: Last-used effect in each slot, parameter values, chain routing (EffectsManager)
    • Persistence: Saved on every effect parameter change (debounced to 500ms)
    • Format: Nested XML similar to mixxx.cfg
  • analysis/: Track analysis cache directory (Track Analysis Pipeline):
    • Structure: Subdirectories by track hash (first 2 chars of MD5)
    • Files: <hash>.dat binary files containing serialized analysis data
    • Contents: BPM beat grid (BPM Detection), key signature (Key Detection), waveform summary (Waveform Generation) (Protocol Buffer format)
    • Size: ~1-3MB per track analyzed
    • Cleanup: Safe to delete—Mixxx re-analyzes tracks as needed (AnalyzerQueue)
  • log/: Application logs (created on startup, Debugging Strategies):
    • Files: mixxx.log (current), mixxx.log.1 (previous), up to 5 rotated logs
    • Rotation: On each startup, mixxx.logmixxx.log.1mixxx.log.2 → ...
    • Content: Debug messages, warnings, errors with timestamps
    • Log levels: Debug, Info, Warning, Critical (configured via --debugLog flag, Command Line Arguments)
    • Size limit: ~10MB per log file
  • Platform-specific paths (Platform Requirements):
    • Linux: ~/.mixxx/ (follows XDG Base Directory spec, Ubuntu 22.04+)
    • macOS: ~/Library/Application Support/Mixxx/ (macOS 11+)
    • Windows: C:\Users\<user>\AppData\Local\Mixxx\ (Windows 10 build 1809+)
  • Override: --settingsPath <path> command-line flag for portable installations (Application Lifecycle)

Application Lifecycle

Startup Sequence (src/main.cpp, Lifecycle and Initialization, Manager Pattern):

  1. QApplication initialization with HiDPI support (Qt Integration Patterns, Core Libraries)
  2. Command-line argument parsing (CmdlineArgs) for debug flags, config paths, locale settings
  3. User settings loaded from ~/.mixxx/ (Configuration Files) via UserSettings (Preferences and Settings)
  4. CoreServices construction - dependency injection root (Core Services Architecture, Manager Pattern)
  5. Managers initialized (Manager Responsibilities): PlayerManager (Mixer), EffectsManager (Effects System), Library (Library), ControllerManager (Controllers), SoundManager (Engine)
  6. MixxxMainWindow created, skin loaded and parsed (Skin/UI, Creating Skins)
  7. Controllers enumerated and opened (ControllerManager, MIDI/HID device discovery)
  8. Audio engine started (Engine, Real-Time Audio Thread Requirements), Qt event loop begins (Three Fundamental Threads)

Shutdown: Reverse order with explicit cleanup to prevent control object leaks (RAII, Memory Management).

Typical User Interaction Flow

Understanding how user actions flow through Mixxx helps visualize the architecture in practice. Here's the story of loading and playing a track:

1. User clicks track in library (GUI Thread):

  • WLibraryTableView receives mouse click event from Qt
  • Emits trackSelected() signal to LibraryControl
  • LibraryControl reads selected row from BaseTrackTableModel
  • Model retrieves TrackId from SQLite via TrackDAO
  • GlobalTrackCache returns existing TrackPointer or creates new one from database

2. User drags track to deck (GUI Thread → Library Thread):

  • Drag gesture creates QMimeData with track location
  • Drop event triggers PlayerManager::slotLoadToDeck(TrackPointer, group)
  • PlayerManager finds appropriate Deck instance for group="[Channel1]"

3. Track loading begins (Library Thread):

  • Deck::slotLoadTrack() called with TrackPointer
  • EngineBuffer::loadTrack() initiated on audio thread via queued signal
  • SoundSourceProxy opens audio file, reads metadata
  • CachingReader spawns worker thread to pre-buffer audio chunks
  • AnalyzerQueue schedules BPM/key analysis if not already done

4. Engine controls initialize (Audio Thread):

  • EngineBuffer emits trackLoaded() signal
    • Broadcast: All EngineControls listening get notified simultaneously
  • CueControl::trackLoaded() loads saved cue points from database
    • Example: If track has hotcue at 30 seconds, hotcue_1_position control is set
  • BpmControl::trackLoaded() reads beat grid, updates bpm control
    • Why: Sync needs to know track BPM immediately
  • LoopingControl::trackLoaded() clears active loops
    • Safety: Old track's loop positions don't apply to new track
  • RateControl::trackLoaded() resets pitch slider position
    • Option: Can be configured to keep rate or reset to 0%

5. UI updates automatically (GUI Thread):

  • Controls emit valueChanged() signals via Qt's queued connections
    • Magic: Signals automatically cross thread boundary (audio → GUI)
  • WLabel widgets displaying artist/title auto-update from track metadata controls
    • No manual wiring: Widgets connected to controls in skin XML
  • WaveformWidget requests waveform summary from analysis cache
    • Pre-computed: Waveform was analyzed when track first scanned
  • BPM display updates from [Channel1],bpm control
    • Observable: BPM change triggers update without polling
  • Deck cover art loads asynchronously
    • Non-blocking: Image loads in background, doesn't freeze UI

6. User presses play button (GUI Thread → Audio Thread):

  • WPushButton receives mouse press event
    • Qt event: Mouse event delivered to widget via Qt event loop
  • Writes 1.0 to [Channel1],play control via ControlProxy
    • Lock-free: Uses QAtomicInteger::store()—no mutex needed
  • ControlDoublePrivate atomically updates value (lock-free)
    • Safe: Audio thread can read while GUI thread writes—no race condition
  • EngineBuffer reads control value in next audio callback (~5ms later)
    • Polling: Audio thread checks control value every callback
  • Audio playback starts, CachingReader streams chunks from disk
    • Background I/O: Disk reading happens in separate thread

7. Audio processing loop (Audio Thread, every ~5-20ms):

  • SoundManager calls EngineMixer::process(buffer, size)
    • Hardware callback: Sound card says "I need 512 samples NOW"
  • EngineMixer iterates all EngineChannel instances
    • For each: Deck 1, Deck 2, Samplers, Microphone, Auxiliary inputs
  • EngineBuffer::process() reads audio from CachingReader
    • Lock-free ring buffer: Pre-loaded audio ready to read instantly
  • EngineBufferScale applies pitch/tempo adjustments via RubberBand or SoundTouch
    • Key lock: Can play 120 BPM track at 128 BPM without chipmunk effect
  • EngineFilterBlock applies EQ (high/mid/low)
    • 3-band: Low/mid/high filters with kill switches
  • EffectChain processes audio through enabled effects
    • Serial: Effect 1 output → Effect 2 input → Effect 3 input → ...
  • Channel audio summed to master, delivered to soundcard
    • Mix: Deck1 + Deck2 + Samplers → Master EQ → Master Effects → Output
  • Position control atomically updated for waveform display
    • GUI reads this: Waveform position updates from this control

8. Controller LED feedback (Audio Thread → Controller Thread):

  • EngineBuffer updates [Channel1],play_indicator control
    • Different from play: play_indicator reflects actual playback state
  • ControlObject emits valueChanged() to all listeners
    • Broadcast: GUI, controllers, scripts all notified simultaneously
  • Controller::slotControlValueChanged() receives queued signal
    • Thread-safe: Signal automatically queued from audio → controller thread
  • MIDI output message sent to hardware (LED lights up)
    • Bidirectional: Controller sees what's happening in Mixxx

9. Waveform rendering (GUI Thread):

  • WaveformWidget::paintEvent() called at 60fps
    • Qt repaint: Widget redraws 60 times per second for smooth animation
  • Reads current playback position from [Channel1],playposition control
    • Lock-free read: No mutex—just atomic read of double value
  • Retrieves pre-analyzed waveform data from cache
    • Pre-computed: Waveform summary (compressed) stored in database
  • OpenGL shader renders scrolling waveform with beat markers
    • GPU-accelerated: Waveform rendering happens on graphics card
  • No audio thread interaction during rendering (reads atomic controls only)
    • Decoupled: Audio thread never waits for GUI—keeps running smoothly

Key Architectural Points:

  • Control System (src/control/) is the communication hub—all subsystems talk via controls
    • Universal interface: UI, scripts, controllers all use same ConfigKey addressing
    • No direct coupling: Subsystems don't know about each other, only controls
  • Thread separation prevents GUI operations from blocking audio processing
    • Independent: GUI can freeze for 100ms, audio still plays perfectly
    • Consequence: Never call slow functions (file I/O, network) from audio thread
  • Qt signals/slots automatically route cross-thread calls via event queues
    • Automatic marshalling: Qt handles thread boundary crossing for you
    • No manual mutex: Just connect signal to slot, Qt does the rest
  • TrackPointer shared ownership ensures track stays valid across all subsystems
    • Reference counting: Track only deleted when last pointer released
    • Safety: Can't access deleted track—pointer becomes null
  • Lock-free atomics in controls enable audio thread to read GUI state without blocking
    • QAtomicInteger: Reads/writes are atomic CPU instructions—no locks
    • Performance: ~1ns to read atomic vs ~50ns for mutex lock
  • Queued signals provide thread-safe async communication without explicit locking
    • Message passing: Signal queued as event, delivered later on target thread
    • No shared state: Threads don't modify same memory, they send messages
  • Observer pattern through controls means adding new features doesn't require changing existing code
    • Open/closed principle: Open for extension, closed for modification
    • Example: Add new controller—just connect to existing controls, no Engine changes

Qt Integration Patterns

Mixxx is built as a Qt application and deeply integrates with Qt's frameworks to achieve its multi-threaded, event-driven architecture. Qt provides the foundation for Mixxx's thread safety, object lifecycle management, and cross-subsystem communication. Understanding Qt's patterns is essential for Mixxx development since nearly every subsystem relies on signals/slots for loose coupling, object trees for automatic memory management, and connection types for thread-safe communication.

The Qt Meta-Object Compiler (MOC) processes classes marked with the Q_OBJECT macro, generating additional C++ code that enables Qt's runtime introspection, signal/slot mechanism, and property system. This meta-object system is fundamental to Mixxx's architecture—it allows Control System objects to emit valueChanged() signals that automatically propagate to Skin/UI widgets, Controllers scripts, and other subsystems without direct coupling.

How Mixxx uses the Meta-Object System:

  • ControlObject and ControlProxy emit valueChanged(double) signals when control values change, enabling Observer Pattern
  • Library models (BaseTrackTableModel, PlaylistTableModel) emit dataChanged() signals to update QTableView widgets automatically
  • Controllers use QJSEngine with meta-object integration to expose C++ objects to JavaScript (MIDI Scripting)
  • EngineControl subclasses (CueControl, LoopingControl) connect to track loading signals and respond automatically
  • UserSettings inherits QObject for signal emission when preferences change, triggering UI updates across the application
class MyControl : public QObject {
		Q_OBJECT  // enables signals/slots, requires MOC processing
	public:
		MyControl();
	signals:
		void valueChanged(double);  // can be connected to any number of slots
	public slots:
		void slotSetValue(double value);  // can be called via signals or directly
};

Build System Integration: CMakeLists.txt automatically processes Q_OBJECT classes via the AUTOMOC setting, which scans headers for Q_OBJECT macros and runs MOC automatically during compilation. Custom classes in new subsystems may require explicit qt_wrap_cpp() calls if not auto-detected.

Qt's signal/slot mechanism is Mixxx's primary tool for decoupling subsystems and achieving thread-safe communication. When a signal is emitted, Qt automatically invokes all connected slots, with the connection type determining whether the call is synchronous (same thread) or asynchronous (cross-thread via event queue). This mechanism is used extensively throughout Mixxx—from ControlObject value changes propagating to UI widgets, to Library track metadata updates refreshing the track table, to Controllers triggering playback actions.

Standard Qt connection syntax (compile-time type-checked):

connect(sender, &SenderClass::signalName,
				receiver, &ReceiverClass::slotName,
				Qt::ConnectionType);

Real Mixxx examples:

// ControlObject notifies all listeners when value changes
connect(m_pControlObject.get(), &ControlObject::valueChanged,
				this, &WPushButton::slotControlValueChanged,
				Qt::AutoConnection);  // UI updates - queued if cross-thread

// Track loading triggers engine controls to reset state
connect(pTrack.get(), &Track::loaded,
				this, &CueControl::trackLoaded,
				Qt::DirectConnection);  // Same thread, immediate callback

// Library scanner updates UI progress
connect(m_pScanner, &LibraryScanner::scanProgress,
				this, &DlgLibrary::slotScanProgress,
				Qt::QueuedConnection);  // Cross-thread: scanner → GUI thread

Connection Types (critical for Thread Safety):

  • Qt::AutoConnection (default): Qt automatically chooses based on thread affinity

    • Uses DirectConnection if sender and receiver are in the same thread
    • Uses QueuedConnection if sender and receiver are in different threads
    • Most common in Mixxx for UI updates and general subsystem communication
    • Safe choice when thread relationship is uncertain
  • Qt::DirectConnection: Immediate synchronous call in sender's thread

    • Slot executes immediately as part of the emit statement (like a function call)
    • Required for Audio Thread callbacks that need sample-accurate timing
    • Used when audio engine controls need to respond to changes within the same buffer
    • Dangerous if receiver does slow operations (blocks sender's thread)
    • Example: ControlProxy reading from ControlObject in audio thread
  • Qt::QueuedConnection: Asynchronous queued event for cross-thread communication

    • Slot executes later in receiver's event loop (thread-safe message passing)
    • Essential for GUI Thread updates from Audio Thread (prevents race conditions)
    • Used when Library Thread signals database changes to UI widgets
    • Adds ~1ms latency (acceptable for UI, unacceptable for audio)
    • Example: Track metadata changes from library scanner to track table view
  • Qt::BlockingQueuedConnection: Queued but sender blocks until slot completes

    • Rarely used in Mixxx due to potential deadlock risks
    • Necessary when cross-thread call must complete before sender continues
    • Can cause Audio Thread underruns if misused

Thread Safety Rules in Mixxx:

  1. Audio threadGUI thread: Always use Qt::QueuedConnection (or AutoConnection with different threads)
  2. GUI threadAudio thread: Write to ControlObject using lock-free atomics (never direct slots)
  3. Same thread callbacks needing immediate response: Use Qt::DirectConnection
  4. Uncertain thread relationship: Use Qt::AutoConnection (safe default)

Value Change Request Pattern

The Value Change Request Pattern is a Mixxx-specific extension to Qt's signal/slot system that enables validation and coordination before accepting a control value change. This pattern is critical for controls where multiple subsystems might want to veto or modify a requested change—for example, preventing a deck from seeking during recording, or quantizing a loop position to beat boundaries.

How it works: Instead of directly setting a control value, clients emit a request signal. The control owner can validate, transform, or reject the request before confirming the actual value change. This prevents race conditions where multiple threads might try to set conflicting values simultaneously.

Mixxx usage examples:

  • LoopingControl: Validates that loop endpoints are within track boundaries before setting
  • BpmControl: Quantizes tempo changes to beat fractions when quantize mode is enabled
  • RateControl: Prevents rate changes that would exceed time-stretch limits
  • CueControl: Ensures cue points snap to beat grid when quantize is active
// Control owner connects to validate requests (typically in constructor)
connectValueChangeRequest(this, &MyClass::slotValidate,
												 Qt::DirectConnection);  // immediate validation

// In the slot, validate/transform and confirm
void MyClass::slotValidate(double requestedValue) {
		// Apply business logic
		if (isValid(requestedValue)) {
				double finalValue = transform(requestedValue);  // e.g., quantize to beat
				setAndConfirm(finalValue, this);  // confirm actual value
		}
		// If invalid, request is silently dropped (no change)
}

Benefits:

  • Separation of concerns: UI/controllers request changes, engine validates
  • Thread safety: Validation runs in control owner's thread context
  • Consistency: All change requests go through same validation path
  • Atomicity: Request and validation happen together (no race conditions)

Qt Smart Pointers

Qt provides reference-counted smart pointers that integrate with Qt's object tree and thread safety mechanisms. Mixxx uses Qt smart pointers extensively for Memory Management of shared objects, particularly for tracks, controls, and settings that need to be accessed from multiple threads safely.

Qt Smart Pointer Types:

QSharedPointer<T>    // reference-counted shared ownership (thread-safe refcount)
QWeakPointer<T>      // non-owning weak reference (doesn't prevent deletion)
QScopedPointer<T>    // auto-delete on scope exit (move-only, like unique_ptr)

How Mixxx uses Qt Smart Pointers:

  • QSharedPointer<Track> (TrackPointer): Ensures one canonical Track object per file via GlobalTrackCache

    • Multiple subsystems (Engine, Library, UI) hold pointers to same track
    • Reference counting ensures track object lives as long as any subsystem needs it
    • Thread-safe atomic reference count allows safe cross-thread sharing
    • Automatic cleanup when last pointer released
  • QSharedPointer<ControlDoublePrivate>: Controls remain valid across all ControlProxy instances

    • Audio thread holds proxy references without worrying about GUI thread deleting control
    • Weak pointers in global registry prevent memory leaks
    • Atomic refcount allows lock-free control access
  • QWeakPointer<T>: Non-owning references that don't prevent deletion

    • Used in GlobalTrackCache registry to allow tracks to be cleaned up
    • Caches hold weak pointers; strong pointers created on demand
    • Prevents circular reference cycles that would cause memory leaks

Mixxx Custom Smart Pointer Typedefs:

  • parented_ptr<T>: Enforces Qt object tree management (no manual delete needed)

    • Automatically deletes child when parent is destroyed (RAII)
    • Used for UI widgets, controllers, and QObject-derived classes
    • Compiler error if you try to explicitly delete (prevents double-free bugs)
  • TrackPointer: Typedef for QSharedPointer<Track> (Library)

    • Never use raw Track* pointers in Mixxx code
    • Pass by value (cheap atomic refcount increment)
    • Return from functions by value
  • UserSettingsPointer: Typedef for QSharedPointer<UserSettings> (Preferences and Settings)

    • Shared configuration access across all subsystems
    • Thread-safe reading/writing of user preferences

Memory Safety Pattern:

// CORRECT: Use TrackPointer for all Track references
TrackPointer pTrack = m_pTrackCollection->getTrackByRef(trackRef);
if (pTrack) {
		pTrack->setTitle("New Title");  // safe even if track deleted elsewhere
}

// WRONG: Never use raw pointers
Track* pTrack = m_pTrackCollection->getTrackByRef(trackRef).get();  // DON'T DO THIS
// pTrack might become dangling if last QSharedPointer is released

Key Architectural Patterns

Dependency Injection:

  • Constructor injection: All major subsystems receive dependencies through constructor parameters rather than creating internally
  • DI root: CoreServices (src/coreservices.cpp) serves as dependency injection root
  • Manager instantiation: PlayerManager, EffectsManager, Library, ControllerManager instantiated once at startup, passed to requiring objects
  • Testing: Enables unit tests to inject mock implementations (src/test/), decouples object lifecycles
  • Example: EngineBuffer receives pointers to EngineControls, EngineEffectsManager, SyncControl rather than instantiating
  • Benefits: Isolated testing, flexible configuration

Factory Pattern:

  • Abstraction: Object creation abstracted through factory methods encapsulating complex initialization
  • Returns: Fully-configured objects without exposing construction details
  • Examples: PlayerManager::addDeckPlayer() (src/mixer/playermanager.cpp), EffectsManager::createEffectChain(), SoundSourceProxy::createSoundSource()
  • Deck creation: Creates deck instances with all required engine components, controls, signal connections
  • Format handling: Format-specific instantiation (audio codecs, effect types)
  • Benefits: Allows adding new types without modifying client code, centralizes creation invariants

Singleton:

  • Purpose: Critical shared resources with exactly one instance per application
  • Examples: GlobalTrackCache (src/track/globaltrackcache.cpp), ControlDoublePrivate registry (src/control/control.cpp)
  • GlobalTrackCache: Maintains canonical Track object per file path, prevents concurrent conflicting modifications
  • Control registry: Global ConfigKey → control instance mapping, any subsystem can access controls by string name
  • Thread safety: Lazy initialization with std::call_once, ensures single instantiation in multi-threaded scenarios
  • Cleanup: QWeakPointer references prevent destruction order issues

RAII:

  • Principle: Resource acquisition is initialization—resources acquired in constructors, released in destructors
  • Cleanup: Deterministic cleanup even during exception unwinding
  • Smart pointers: std::unique_ptr (unique ownership), QSharedPointer (shared ownership), parented_ptr (Qt object tree)
  • Benefits: Eliminates manual delete calls, prevents leaks
  • Scoped locks: QMutexLocker, std::lock_guard automatically release mutexes when leaving scope
  • Transactions: SqlTransaction RAII wrapper commits on normal exit, rolls back on exception

Observer Pattern:

  • Implementation: Control System (src/control/controlobject.h) implements observer pattern
  • Signal emission: ControlObject emits Qt valueChanged(double) signals whenever control values change
  • Observers: Multiple observers (UI widgets, controller scripts, skin elements, other controls) connect slots to react
  • Decoupling: Producers decoupled from consumers—EngineBuffer sets playback position without knowing what UI displays it
  • Connection types: Qt::DirectConnection (same-thread), Qt::QueuedConnection (cross-thread), Qt::AutoConnection (automatic)
  • Synchronization: DirectConnection synchronous (immediate), QueuedConnection asynchronous (event queue)

Bridge Pattern:

  • Classes: ControlObject (src/control/controlobject.cpp) and ControlProxy (src/control/controlproxy.cpp) form bridge
  • Separation: Control interface separated from implementation, enables different access semantics per thread
  • ControlObject (implementor): Ownership semantics, creates ControlDoublePrivate, registers in global map, emits signals, provides parameter transformation
  • ControlProxy (abstraction): Non-owning access, finds controls by ConfigKey, caches pointer, accesses values through atomic operations
  • Thread usage: GUI thread uses ControlObject for lifecycle management, audio thread uses ControlProxy for lock-free access
  • Benefits: Control creation/destruction on GUI thread while audio thread maintains stable pointers via atomic refcounting

Proxy Pattern:

  • Purpose: ControlProxy (src/control/controlproxy.cpp) acts as surrogate for ControlObject
  • Thread-safe access: Provides access without ownership or lifecycle concerns
  • Resolution: Resolves ConfigKey to ControlDoublePrivate pointer once at construction, caches for fast repeated access
  • Operations: All value operations (get(), set(), getParameter()) use atomic QAtomicPointer
  • Lock-free: Enables lock-free access from audio thread while GUI modifies control properties
  • Validity tracking: If underlying control deleted, proxy operations become no-ops (no crashes)
  • Benefits: Protects audio thread from GUI control lifecycle changes, eliminates mutexes in timing-critical paths

Command Pattern:

  • Implementation: MIDI/HID controller mappings (src/controllers/midi/midicontroller.cpp, src/controllers/hid/hidcontroller.cpp)
  • Encapsulation: Input events encapsulated as command objects binding hardware inputs to control actions
  • Mapping storage: MidiInputMapping stores MIDI pattern (status/note/channel), target ConfigKey, transformation parameters
  • Separation: Command definition separated from execution
  • Execution: MidiController::receive() looks up mappings, executes commands via ControlObject::set()
  • JavaScript: engine.setValue() calls become command objects queued for script thread
  • Features: Undo via control value history, macro commands, conditional execution

DAO Pattern:

  • Encapsulation: All SQL database operations encapsulated in Data Access Object classes
  • One entity per DAO: Each DAO responsible for one entity type
  • Examples: TrackDAO (src/library/trackdao.cpp), CueDAO (src/library/dao/cuedao.cpp), PlaylistDAO
  • Operations: getTrackByRef() executes SELECT with joins, saveTrack() performs UPDATE/INSERT
  • SQL isolation: Isolates SQL from business logic
  • Initialization: Each DAO receives QSqlDatabase reference, prepares parameterized statements (prevents SQL injection)
  • Type safety: Returns domain objects (Track, Cue) rather than raw QSqlQuery results
  • Benefits: Isolates SQL dialect specifics, enables schema changes without modifying call sites, allows swapping databases

Facade Pattern:

  • Purpose: Complex subsystems expose simplified interfaces hiding internal complexity
  • TrackCollection: Unified API (getTrackByRef(), saveTrack()) coordinates multiple DAOs, GlobalTrackCache lookups, transaction management (src/library/trackcollection.cpp)
  • Library facade: Library (src/library/library.cpp) facades entire library subsystem
    • Track scanner thread management
    • Analysis queue
    • External storage detection
    • Model/view coordination
  • Simple operations: "add directory", "get track" hide complex orchestration
  • Benefits: Callers use intuitive high-level methods, reduces coupling, centralizes orchestration logic

Adapter Pattern:

  • Purpose: Different audio file formats adapted to uniform AudioSource interface (src/sources/audiosource.h)
  • Formats: MP3, FLAC, AAC, OGG, WAV, stems, module files
  • Implementation: Format-specific SoundSource implementations
    • SoundSourceFLAC (src/sources/soundsourceflac.cpp) wraps libFLAC decoder
    • SoundSourceMP3 wraps libmad/libmpg123
  • Common operations: readFrames(), seekFrame(), channelCount()
  • Selection: SoundSourceProxy (src/sources/soundsourceproxy.cpp) selects adapter by file extension, returns generic AudioSource pointer
  • Extensibility: Adding new format requires only implementing SoundSource interface and registering, no engine changes

Strategy Pattern:

  • Purpose: Control value mapping between normalized [0,1] parameter space and physical units
  • Implementation: Pluggable ControlBehavior strategies (src/control/controlbehavior.h)
  • Strategies:
    • ControlLinBehavior: Linear scaling
    • ControlLogBehavior: Logarithmic curves for volume/EQ (human perception)
    • ControlPotmeterBehavior: Min/max/center positions with different curves per range
  • Selection: Controls select behavior at construction
  • Examples: ControlPotmeter (knobs with center detents), ControlLinPotmeter (sliders)
  • Benefits: Changing mapping requires only swapping behavior object, parameter transformation reused, new algorithms via subclassing

Template Method:

  • Purpose: Define algorithm skeleton with subclasses overriding specific steps
  • Implementation: EngineControl::process() (src/engine/controls/enginecontrol.h) template method for deck feature processing
  • Orchestration: EngineBuffer::process() (src/engine/enginebuffer.cpp) calls process() on each EngineControl in fixed order every audio callback
  • Controls: CueControl, LoopingControl, BpmControl, RateControl
  • Flow: Pre-process setup → each control processes → post-process cleanup
  • Customization: Each control's process() reads state, performs feature-specific logic, writes output controls
  • Benefits: EngineBuffer doesn't know control details, just invokes process() on all

Composite Pattern:

  • Purpose: Organize objects hierarchically, treat uniformly
  • Implementation: EffectChain (src/effects/effectchain.cpp) contains multiple EffectSlot objects, each containing EffectProcessor
  • Tree structure: Forms tree treated uniformly
  • Processing: EffectChain::process() iterates child slots calling slot->process()processor->process()
  • Common interface: Both EffectChain and EffectSlot implement common interface (process audio, enable/disable, parameter access)
  • Nesting: Allows chains to be nested (currently single-level)
  • Benefits: Adding/removing effects doesn't require modifying chain logic, enables complex routing without conditional logic

Mediator Pattern:

  • Purpose: Mediate communication between components, prevent direct coupling
  • Implementation: EngineMaster (src/engine/enginemaster.cpp) mediates all communication between engine channels
  • Channels: Decks, samplers, preview deck, microphone inputs
  • Communication: Decks communicate through EngineMaster rather than directly
    • SyncControl notifies EngineMaster of BPM changes → propagates to synced decks
    • EngineDelay queries master output latency for mic monitoring
    • Crossfader reads left/right volumes from EngineMaster
  • Benefits: Simplifies adding channels, modifying routing, implementing global features, reduces many-to-many to hub-spoke

Chain of Responsibility:

  • Purpose: Pass requests through chain of handlers
  • Implementation: Audio buffer processing flows through chain of EngineControl objects
  • Sequence: RateControlBpmControlSyncControlClockControlCueControlLoopingControl → final RateControl
  • Processing: Each control examines state, handles responsibility, allows subsequent controls to process
  • Parameter modification: Controls can modify parameters for downstream handlers (rate affects loop position)
  • Conditional: If control doesn't handle aspect, processing continues unaffected
  • Order matters: Sync must run before loop position updates
  • Benefits: Extends deck functionality by adding new EngineControl subclasses without modifying existing controls

These patterns compose to create a resilient, maintainable architecture: dependency injection enables unit testing with mock managers (src/test/), the control system decouples subsystems while maintaining thread safety across GUI/audio/library threads, and template method with chain of responsibility allow deck features to extend processing without modifying EngineBuffer. The observer pattern keeps UI widgets (src/widget/), controller scripts (res/controllers/), and skins (res/skins/) synchronized with engine state via controls, while DAO pattern isolates SQL complexity from business logic. Understanding these patterns is essential for adding features that integrate cleanly with Mixxx's existing architecture.

Coding Style Essentials

Naming Conventions (Code Organization):

  • Classes: PascalCase (e.g., ControlObject, TrackDAO, Control System)
  • Methods: camelCase (e.g., getValue(), setPosition())
  • Member variables: m_camelCase (e.g., m_pControl, m_value)
  • Pointer variables: p prefix (e.g., pTrack, pControl, Track Pointer Pattern)
  • Config items: snake_case (e.g., hotcue_1_position, beat_active_0_5, ConfigKey)
  • Constants: kPascalCase or UPPER_CASE (e.g., kNoPosition, CSAMPLE_ZERO, Type Definitions)

Memory (Memory Management):

Threading (Threading Model, Thread Safety Considerations):

Headers:

Include Order Example:

#pragma once  // header guard (not #ifndef)

#include <QObject>     // Qt headers
#include <memory>      // standard library headers

#include "control/controlobject.h"  // Mixxx headers
#include "util/types.h"              // Core type definitions

Assertions:

  • DEBUG_ASSERT(condition) - debug builds only (assert)
  • VERIFY_OR_DEBUG_ASSERT(cond) { fallback } - always checked
  • RELEASE_ASSERT(condition) - fatal, always enabled
  • Liberal use encouraged for catching bugs early

Comment Style:

  • Laconic but descriptive
  • Lowercase except proper nouns
  • // MARK: SECTION TITLES with 2+ blank lines above
  • // MARK: -- subsection titles in lowercase
// user-oriented comments are laconic but descriptive
// lowercase except for capitalized nouns


// MARK: MAJOR SECTION TITLES
// (minimum 2 blank lines above MARK comments)


// MARK: -- subsystem titles in lowercase

Modern C++:

Extension Points

Adding Features (Integration Examples, Adding New Features: Quick Checklist):

  1. New controls in EngineControl subclass (Engine, EngineBuffer, inheritance)
  2. New effects in src/effects/backends/builtin/ (Effects System, Built-In Effects)
  3. New skin widgets by subclassing WBaseWidget (Skin/UI, Widget and UI Patterns)
  4. New library features via new DAO classes (Library, DAO Pattern)
  5. New controller mappings via XML + JavaScript (Controllers, MIDI Scripting)
  6. New file formats via decoder plugins (Audio File Formats and Extensions, SoundSource)

API Stability (Deprecations and Migrations, Version History):


Major Subsystems

Manager Pattern

Mixxx uses manager objects as service coordinators and dependency injection containers, implementing a form of the Facade Pattern where each manager provides a simplified interface to a complex subsystem. This pattern centralizes object creation (Factory Pattern), lifecycle management (RAII), and cross-subsystem coordination, preventing the chaotic "everything talks to everything" anti-pattern.

Why managers are essential in Mixxx:

  • Single source of truth: PlayerManager is the only place that creates decks—no hidden deck instances scattered across the codebase
  • Dependency injection: Managers receive dependencies via constructors, making unit testing possible with mock managers (Testing with Mock Managers)
  • Clear initialization order: CoreServices orchestrates manager startup in the correct dependency order (Application Lifecycle)
  • Centralized cleanup: Qt object trees ensure managers delete their owned objects automatically (Memory Management)
  • Cross-subsystem coordination: When a track loads, PlayerManager coordinates between Library, Engine, and Effects without tight coupling

Each manager corresponds to a major subsystem (Major Subsystems): PlayerManager for Mixer, Library for Library, ControllerManager for Controllers, etc. This one-to-one mapping makes the architecture easy to understand and navigate.

(Note: The full Manager Pattern documentation with Core Services Architecture, Manager Responsibilities, Lifecycle, Ownership Model, Dependency Injection, Communication Patterns, Complete Example, Thread Safety, and Testing sections has been moved here from its previous location after the Major Subsystems section. It now appears before the Control System to establish the architectural context.)


Engine (src/engine/)

The Engine subsystem is Mixxx's real-time audio processing core, responsible for reading audio from files, applying effects, synchronizing decks, and delivering glitch-free output to the sound card. It runs in a separate high-priority thread and must complete all processing within ~5-20ms to avoid audio dropouts. Understanding the Engine is essential for anyone working on playback features, sync, effects, or performance optimization.

Why This Matters: The Engine processes audio in real-time at microsecond precision. Any blocking operation (mutex locks, file I/O, memory allocation) in the audio thread causes audible glitches. All Engine code must be lock-free and use pre-allocated buffers. See Real-Time Audio Thread Requirements for critical constraints.

  • EngineMixer (.cpp) - Master mixer

    • Top-level coordinator owning all EngineChannel instances (decks, samplers, aux, mics)
      • Think of this as the conductor of an orchestra—coordinates all audio sources
      • Relationship: Managed by SoundManager, which handles audio hardware interface
      • Pattern: Uses Mediator Pattern—channels don't talk to each other directly
    • process(CSAMPLE* pInOut, const int bufferSize): Main audio callback (real-time processing)
      • Called: Every 256-2048 samples (~5-20ms at 44.1kHz sample rate)
      • Parameters: pInOut is input/output buffer (interleaved stereo CSAMPLE array), bufferSize is number of stereo frames
      • Returns: void (modifies buffer in-place for zero-copy efficiency)
      • This is the heartbeat of Mixxx—happens hundreds of times per second
      • Critical constraint: Must finish in < 20ms or you hear crackles/pops (Real-Time Audio Thread Requirements)
      • Buffer: Larger = more latency but safer; smaller = lower latency but riskier
      • Thread: Runs on Audio Thread at high RT priority
    • addChannel(EngineChannel* pChannel): Register audio source for mixing
    • Sums all channels to stereo master, applies master EQ/effects, delivers to sound card
      • Flow: Deck 1 + Deck 2 + Samplers → Master EQ → Master Effects → Speakers
    • Single-threaded, runs at high RT (real-time) priority
      • Why single-threaded: Avoids complexity and synchronization overhead
      • Priority: OS gives this thread first access to CPU—critical for glitch-free audio (Threading Model)
  • EngineBuffer (.cpp) - Per-deck buffer manager

    • One EngineBuffer exists for each deck—this is your deck's "brain"
      • Relationship: Created by EngineMixer, receives track data from Library (Library)
      • Ownership: Contains all the moving parts needed to play a track (Composition Pattern)
    • process(CSAMPLE* pOutput, const int iBufferSize): Per-deck audio generation
      • Called: By EngineMixer::process() for each active deck every audio callback
      • Parameters: pOutput is output buffer for this deck's audio, iBufferSize is frames to generate
      • Processing: Reads from CachingReader, applies time-stretch, processes EngineControl chain
      • Thread-safety: Lock-free reads from ring buffer via atomic operations (Control Access Patterns)
    • loadTrack(TrackPointer pTrack): Load new track into deck
      • Called: When user drags track to deck or uses LoadSelectedTrack control
      • Parameters: pTrack is TrackPointer (thread-safe shared ownership)
      • Actions: Signals CachingReader to load new file, resets playback position, triggers trackLoaded() on all EngineControl instances
      • Thread: Can be called from GUI thread, coordinates with audio thread via atomics
    • CachingReader: Disk I/O in separate thread (Bridge Pattern)
      • Problem solved: Reading from disk is slow (~10ms), but audio callback needs data NOW
      • Solution: Background thread constantly fills a ring buffer 1-5 seconds ahead
      • Ring buffer: Circular queue—when you reach the end, wrap back to start (lock-free SPSC queue)
      • read(CSAMPLE* pBuffer, int iFrames): Request audio data from cache
        • Called: By EngineBuffer::process() every audio callback
        • Lock-free: Uses atomic indexes, never blocks (Real-Time Audio)
      • hintAndMaybeWake(HintVector& hints): Signal upcoming read positions
        • Called: Before seeks, loops, cues to pre-load upcoming audio
        • Optimization: Prevents cache misses on position jumps
      • Handles seeks (jumping to cue points), reverse play, tempo changes
      • Why this matters: Audio thread never waits for disk—just reads pre-loaded buffer
    • EngineBufferScale*: Time-stretch/pitch processing
      • Purpose: Play a 120 BPM track at 128 BPM without sounding like chipmunks
      • SoundTouch: Faster algorithm, slight quality loss—good for real-time
        • Single-threaded: Processes on main audio thread
      • RubberBand: Much better quality—preferred for key lock
        • Multithreaded (main branch): Uses worker thread pool for parallel processing
        • Per-stem processing: Each stem (drums, bass, vocals, melody) processed in parallel
        • Performance: Can utilize multiple CPU cores—reduces audio thread load
        • Quality modes: "Faster" vs "Finer" (R3) quality settings
      • Key lock: Keep original pitch when changing tempo (complex DSP magic)
      • User configurable: Can switch between engines in preferences
    • Multiple EngineControl instances: Feature implementations
      • Composition pattern: Instead of one giant class, split into focused components
      • Examples: CueControl handles hotcues, LoopingControl handles loops
      • Processing order matters: Rate changes affect position calculations
      • All processed every audio callback (~5-20ms)—must be fast!
  • Process pipeline: Multi-stage audio processing chain (Template Method)

    • Stage 1: Read audio from CachingReader ring buffer (lock-free)
    • Stage 2: Apply rate/pitch adjustments via EngineBufferScale
    • Stage 3: Call all child EngineControl::process() in sequence
    • Stage 4: Mix processed audio to channel output buffer
    • Timing: Entire pipeline must complete within buffer time (5-20ms)
  • FrameInfo: Playback state structure passed to all controls (Audio Type Conventions)

    • FramePos currentPosition: Current playback frame position
    • FramePos trackEndPosition: Track length for loop boundary checking
    • SampleRate sampleRate: Current sample rate (44.1kHz, 48kHz, etc.)
    • ChannelCount channels: Number of channels (typically 2 for stereo)
    • Updated every audio callback before control processing
  • Chain of Responsibility: Controls processed in dependency order (Chain of Responsibility)

    • Order matters: rate changes must propagate before position updates
    • Each control can modify state for downstream controls
    • Failed operations don't prevent subsequent controls from processing
  • EngineControl (.cpp) - Abstract base for deck features

    • Feature isolation: Each feature is separate class via composition over inheritance (Template Method)
      • Composition allows features to be added/removed independently
      • No deep inheritance hierarchies (max 2 levels: EngineControl → subclass)
      • Each control has single, well-defined responsibility
    • Subclasses: Feature implementations with distinct responsibilities
    • CueControl (.cpp): Hotcue and cue point management
      • Main cue: Single "jump to start" cue point (traditional DJ cueing)
        • Like the "return to start" button on a CD player
      • 36 hotcues: Labeled memory points with instant recall
        • Use case: Mark verse, chorus, drop, break—jump instantly during performance
        • hotcue_X_activate: Press to jump, press again to trigger from that point
        • hotcue_X_position: Stored in frames (sample pairs)—sample-accurate positioning
        • hotcue_X_color: RGB color for visual ID—helps find cues quickly
        • Relationship: Reads/writes to CueDAO (database), exposes ControlObjects for UI/controllers
      • Intro/outro markers: Track structure metadata for Auto DJ smooth transitions
        • Intro: Where vocals/energy starts (fade in from previous track here)
        • Outro: Where track starts to fade out (start next track here)
      • Saved loops: Named loops stored with track in database
        • Survive program restart—always available when track loads
      • Responds to track load by restoring cues from database via CueDAO::getCuesForTrack()
    • LoopingControl (.cpp): Loop boundary management
      • Manual loops: User-set start/end points with adjust controls
        • Use case: Loop the breakdown, extend the drop, repeat 8-bar section
        • Can adjust loop start/end while playing (beatmatch first, then fine-tune)
      • Beatloops: Quantized loops (1, 2, 4, 8, 16, 32, 64 beats)
        • Quantize: Automatically snap to beat grid—no need for perfect timing
        • Powers of 2: Makes it easy to double/halve loop length rhythmically
        • Relationship: Reads beat grid from BpmControl, respects quantize setting
      • Beatjump: Skip forward/backward by beat counts
        • Like fast-forward/rewind but musically aligned
      • Loop rolling: Temporary loop (releases when button released)
        • Performance technique: Hold for stutter effect, release to continue
      • Reloop: Jump back to last active loop
        • Quick return to previously set loop—common DJ move
      • Quantize support: Snap loop points to nearest beat grid
        • Prevents off-beat loops that sound wrong
      • Position wrapping when loop is active (seamless looping)
        • Technical: When playback hits loop_out, jumps to loop_in—all in audio thread
    • BpmControl (.cpp): Tempo and sync management
      • BPM detection: Real-time tempo analysis of playing track
        • Continuously monitors beat positions, updates BPM if drift detected
        • Relationship: Works with AnalyzerBeats (offline analysis) for initial beat grid
      • Master sync: Automatic tempo matching between decks
        • Leader/follower: One deck sets tempo, others follow automatically
        • Phase alignment: Not just same BPM—beats happen at same instant
        • Use case: Mix 3-4 decks perfectly in sync without manual beatmatching
        • Complex math: Calculates beat distances, handles fractional beats, tempo curves
        • Handles tempo changes and track loading (picks new leader if needed)
      • Manual BPM adjust: Tap tempo, fine-tune BPM value
        • Tap tempo: Tap button on beats—Mixxx calculates BPM from intervals
        • Fine-tune: Nudge BPM ±0.01 to fix analysis errors
      • Beat grid editing: Shift/scale beat grid alignment
        • Shift: Move whole grid left/right (first beat was detected wrong)
        • Scale: Stretch/compress grid (tempo changes within track)
      • Sync button modes: One-shot sync (match tempo once) vs. sync lock (stay synced)
    • RateControl (.cpp): Pitch/tempo/speed adjustment
      • Rate slider: Primary playback speed control (-100% to +100%)
        • 0% = original tempo, +8% = faster, -8% = slower
        • Most DJ mixing happens within ±8% range
      • Rate direction: Forward/reverse playback toggle
        • Use case: Backspin effect, creative scratching
      • Rate range: Configurable range (6%, 8%, 10%, 24%, 50%)
        • Tradeoff: Smaller range = finer control; larger range = more flexibility
        • Set in preferences—affects slider sensitivity
      • Pitch bend: Temporary rate nudge for manual beatmatching
        • Technique: Hold button to speed up/slow down, release to return to set rate
        • Mimics pushing/pulling vinyl—essential for manual beatmatching
      • Key lock: Maintain pitch while changing tempo (via EngineBufferScale)
        • Without: Faster tempo = higher pitch (chipmunk effect)
        • With: Can play 120 BPM track at 128 BPM and keep original key
        • Relationship: Triggers EngineBufferScale (SoundTouch or RubberBand)
      • Scratch: Vinyl emulation with configurable sensitivity
        • Maps jog wheel or controller movement to playback position
      • Integrates with sync system for tempo-based adjustments
        • When synced, rate slider may be adjusted automatically by BpmControl
    • ClockControl (.cpp): Beat timing and trigger generation
      • Beat triggers: Pulse controls on every beat (beat_active)
      • Fractional tempo triggers: Subdivision/multiplier beats
        • beat_active_0_5: Half tempo (every 2nd beat)
        • beat_active_0_666: 2/3 tempo (triplet feel)
        • beat_active_0_75: 3/4 tempo
        • beat_active_1_25: 5/4 tempo (quintuplet)
        • beat_active_1_333: 4/3 tempo
        • beat_active_1_5: 3/2 tempo (dotted notes)
      • Bar tracking: Multi-beat phrase detection
      • Used by: Controllers for LED sync, effects for beat-triggered modulation
  • Lifecycle: Initialization and track loading behavior

    • Constructor: Each creates own ControlObjects (Creating Controls)
      • Registers controls in global registry
      • Connects internal signals for coordination
      • Sets default values from user preferences
    • Track loading: trackLoaded() signal response
      • Load track-specific data (cues, beat grid, BPM)
      • Reset transient state (loop disabled, position to start)
      • Update controls with track metadata
    • Process cycle: Called every audio callback in fixed order
  • Sample-accurate timing: Precise event triggering (Real-Time Audio Thread Requirements)

    • process() called every 256-2048 samples (5-20ms at 44.1kHz)
    • Can trigger events at exact sample positions (no timing drift)
    • Critical for tight loops, cue point accuracy, sync precision
  • Benefits: Design advantages of composition pattern

    • Independent testing: Each control tested in isolation (Testing Infrastructure)
    • Clear ownership: EngineBuffer owns all controls via Qt object tree (Memory Management)
    • Single responsibility: Each class handles one feature domain
    • Easy extension: Add new features without modifying existing controls
    • Performance: Minimal overhead, direct method calls in audio thread
  • EngineChannel (.cpp) - Channel abstraction

    • Interface: process() produces CSAMPLE buffer, volume/gain controls, 3-band EQ, 4 effects sends (Effects System, Audio Type Conventions)
    • Subclasses: EngineDeck (full player), Sampler (simple trigger), EngineMicrophone/EngineAux (pass-through with ducking)
    • Routing: EngineMixer iterates channels, processes each, sums to master (Mediator Pattern, Mixer)
  • CachingReader (.cpp) - Read-ahead thread

    • Disk I/O isolation: Separate thread prevents audio thread blocking (Three Fundamental Threads)
    • Decoding: Reads compressed audio (MP3/FLAC/OGG/etc), decodes to CSAMPLE (Adapter Pattern)
    • Ring buffer: Fills 1-5 seconds ahead, audio thread reads lock-free (Real-Time Audio Thread Requirements)
    • Features: Forward/backward playback, seeks with preload, variable rate hints
    • Priority: Worker thread priority lower than audio thread to prevent interference

Audio Types (Audio Type Conventions, Strongly-Typed IDs):

  • CSAMPLE: 32-bit float [-1.0, 1.0] peak amplitude, enables headroom for internal processing
  • FramePos: Stereo frame position (1 frame = 2 samples), wraps double with invalid state checking
  • SampleRate: Wraps int Hz value (44100, 48000, etc.), prevents using BPM where Hz expected
  • FrameDiff_t: Signed frame offset for relative positioning
  • SINT: ptrdiff_t for buffer indexing, enables compiler auto-vectorization (SSE/AVX)
  • Benefits: Catch time-vs-samples, mono-vs-stereo, position-vs-offset errors at compile time (Types and Namespaces)

Constraints: Audio thread is lock-free (Real-Time Audio Thread Requirements). Use atomics (Control System), not mutexes. No allocations. No I/O.

Subsystem Integration (how Engine interacts with rest of Mixxx):

  • Real-time processing on Audio Thread:
    • Thread priority: SCHED_FIFO or THREAD_PRIORITY_TIME_CRITICAL (OS-specific)
    • CPU affinity: Can be pinned to specific cores to reduce cache misses
    • Isolation: Never blocks on GUI/Library threads
    • Communication: Via atomic controls only (Control System)
  • Library integration via CachingReader from Library:
    • Async loading: CachingReader worker thread loads audio chunks
    • Ring buffer: Lock-free SPSC queue between I/O thread and audio thread
    • Buffer size: Typically 1-5 seconds of audio pre-buffered
    • Seeks: Jump to cue point triggers cache flush and reload
    • Format handling: SoundSourceProxy selects appropriate decoder (Audio File Formats)
  • Control exposure via Control System (100+ controls per deck):
    • Playback state: play, playposition, track_loaded, track_samples
    • Tempo/sync: bpm, sync_enabled, sync_mode, beat_distance
    • Loop state: loop_enabled, loop_start_position, loop_end_position
    • Cue points: hotcue_1_position through hotcue_36_position
    • Update rate: Position updated every audio callback (~200 Hz at 512 samples)
    • Atomic access: All controls use QAtomicInteger for lock-free reads
  • Effects routing through Effects chains:
    • Send/return: Each deck has 4 effect send levels (post-fader)
    • Signal flow: EngineChannel::process() → effect chains → EngineMixer::process()
    • Dry/wet mixing: Configurable per chain (0% = dry, 100% = wet)
    • Bypass: Zero-latency bypass when effect disabled
    • State isolation: Each channel gets separate EffectState (no cross-deck interference)
  • User input from Controllers and Skin/UI:
    • Input path: Widget/MIDI → ControlProxy::set() → atomic store → audio thread reads
    • Latency: ~5-20ms from button press to audio change (one buffer delay)
    • No blocking: GUI never waits for audio thread
    • Quantize: Optional beat-grid snapping for cues/loops (BpmControl)
  • Channel coordination by EngineMixer for all Mixer channels:
    • Mix algorithm: Simple summation (no panning/routing matrix)
    • Gain staging: Channel volume → EQ → effects → master volume
    • Clipping prevention: Master limiter at end of chain
    • Latency compensation: All channels processed in same callback (phase-aligned)
  • Sample-accurate sync via Master Sync:
    • Beat distance calculation: Leader broadcasts beat position to followers
    • Phase adjustment: Fractional sample adjustment using interpolation
    • Tempo matching: Rate slider auto-adjusted by BpmControl
    • Quantization: All beat-aligned actions snapped to beat grid
  • Separation of concerns across Three Fundamental Threads:
    • Audio thread: Reads controls, never writes (except position/VU meters)
    • GUI thread: Writes controls, reads via signal/slot connections
    • Library thread: Provides track data asynchronously via TrackPointer
    • No shared state: Only atomic controls bridge threads

For comprehensive implementation details including engine object hierarchies, audio buffer processing, and master sync, see Engine and Audio Processing Details. Threading constraints and lock-free patterns are covered in Audio Thread and Real-Time Audio Thread Requirements. Performance tuning strategies including per-feature costs and profiling are detailed in Performance Optimization. Critical mistakes to avoid are cataloged in Anti-Patterns.

History (Performance Characteristics, Audio Buffer Processing, CHANGELOG.md):

  • Pre-1.0 (2001-2007): Single-deck architecture with basic time-stretching via SoundTouch
  • 1.x series (2008-2011): Multi-deck support, vinyl control integration, basic sync functionality
  • 2.0 (2012): Major refactoring to lock-free audio thread design, introduced ControlProxy for thread-safe parameter access
  • 2.1-2.3 (2013-2019): Master sync algorithm improvements, beatgrid refinements, key lock quality enhancements
  • 2.4 (2024): Performance optimizations, improved caching strategies (CachingReader)
  • 2.5.0 (2024-12): Beatloop anchor controls, Rate Tap button, track color cycling, tempo locking, beats undo
    • Beatloop anchor (set/adjust loop from start or end)
    • Track color palette cycling controls (track_color_next/track_color_prev)
    • Tempo locking controls
    • Beats undo functionality
  • 2.6.0 (main branch): STEM file support with multithreaded RubberBand, per-stem controls and effects
    • Advanced stem loading controls
    • Quick effect support on stems

Library (src/library/)

The Library subsystem manages Mixxx's track database, providing fast access to track metadata, cues, beat grids, and playlists. It uses SQLite for persistent storage and maintains an in-memory cache for frequently accessed tracks. The Library runs in its own thread to avoid blocking the GUI or audio threads during database operations.

Why This Matters: The Library uses the DAO pattern to isolate SQL from business logic, making schema changes safe. Understanding TrackPointer (QSharedPointer) is critical—never use raw Track* pointers as they can become dangling when tracks are unloaded. All track access must go through TrackCollection to ensure thread safety.

  • TrackCollection (.cpp) - Central track registry
    • GlobalTrackCache (.cpp): Singleton ensures one Track object per file path
      • Problem solved: Without this, you could have 2+ Track objects for same file with conflicting data
      • Example: Deck 1 loads track, edits cue points; Deck 2 loads same track with old cue points—bad!
      • Relationship: All subsystems (Engine, Library, UI) get tracks through this cache
    • Cache strategy: Weak references allow cleanup when track no longer in use
      • Strong pointer (TrackPointer): Keeps track alive—held by decks, UI, etc.
      • Weak pointer (in cache): Doesn't keep track alive—allows garbage collection
      • When last deck unloads a track, cache weak pointer becomes null—track destroyed
    • Lookup mechanism: Hash map keyed by canonical file path
      • Canonical path: /home/user/Music/track.mp3 not ~/Music/track.mp3
      • Resolves symlinks to real path—handles linked directories
      • Case-sensitive on Linux/macOS, case-insensitive on Windows
      • Handles moved files via path normalization
    • Cache hit: Returns existing TrackPointer (increments refcount)
      • Fast—just pointer copy, no database query
    • Cache miss workflow: (expensive, involves database and file I/O)
      1. Check database via TrackDAO::getTrackByRef()—is this track in our library?
      2. If found: Create Track object, populate from database (BPM, cues, rating, etc.)
      3. If not found: Create empty Track, mark as new (will be added on save)
      4. Parse metadata tags via taglib (ID3, Vorbis, MP4 atoms)
        • Artist, title, album, year, genre, cover art
        • This hits disk—can take 1-10ms per track
      5. Restore analysis cache (beat grid, waveform, key, ReplayGain)
        • Beat grid from Protocol Buffer binary blob in database
        • Waveform summary (compressed) for scrubbing/overview
        • Key detection for harmonic mixing
        • ReplayGain for volume normalization
      6. Insert into cache with weak pointer, return strong pointer to caller
  • Cache key: Canonical file path ensures consistent tracking
    • Resolves ., .., symlinks to absolute real path
    • Platform-specific path normalization (Windows drive letters, UNC paths)
    • Used for both cache lookup and database foreign key
  • Prevents conflicts: Ensures no concurrent modifications to same track (Thread Safety Considerations)
    • Only one mutable Track object exists per file
    • Multiple subsystems hold TrackPointer (shared read-only access)
    • Modifications coordinated through Track object's internal mutex
    • Database writes atomic via SQL transactions
  • Weak references: Cache uses TrackWeakPointer for automatic cleanup
    • Benefits: Track deleted when last strong pointer released
      • No manual memory management—just drop the TrackPointer
    • Cache eviction: Weak pointers automatically become null when Track deleted
      • Cache doesn't prevent garbage collection
    • Lazy cleanup: Dead weak pointers removed on next cache access
      • No background thread scanning for dead entries—happens naturally
    • Memory efficiency: Tracks not in use don't consume memory
      • 100,000 track library doesn't mean 100,000 Track objects in RAM
      • Only tracks currently loaded in decks/playlists/visible UI are in memory
  • Thread safety: Internal mutex protects cache map
    • Challenge: Library thread adds tracks, GUI thread displays, Engine loads
    • Solution: QMutex serializes access—simple but effective
    • Lock held briefly (just hash lookup)—minimal contention
    • Safe for simultaneous lookups from GUI and library threads
    • Not used by audio thread: Audio thread uses already-loaded TrackPointer
    • Lock scope: Minimal (only during map access, not during track loading)

Data Access Objects (DAO Pattern):

  • TrackDAO (.cpp): Track table CRUD operations (QtSql)

    • Database operations: Wraps QSqlDatabase for type-safe track access
      • getTrackByRef(const TrackRef& trackRef): Load track by file location + optional ID
        • Parameters: trackRef contains file path and optional TrackId
        • Returns: TrackPointer (null if not found, Track Pointer Pattern)
        • SQL: SELECT from library table with LEFT JOIN to track_locations
        • Caching: Checks GlobalTrackCache before hitting database
      • saveTrack(Track* pTrack): INSERT or UPDATE based on track ID state
        • Parameters: pTrack is mutable track object with dirty metadata
        • Returns: bool (true on success, false on SQL error)
        • Transaction: Wrapped in SQL transaction for atomicity (DAO Pattern)
        • Cascading: Triggers save of cues, beat grid, waveform if modified
      • removeTracks(const QList<TrackId>& trackIds): Batch delete with cascading cue removal
        • Parameters: trackIds is list of database primary keys to delete
        • Returns: int (number of tracks deleted, may be less than requested)
        • Foreign keys: Cascades to cues, playlists, crates via ON DELETE CASCADE
        • Performance: Uses prepared statement with batch binding for speed
      • detectMovedTracks(): Finds tracks at new paths (file hash matching)
        • Called: During library scan when files are missing from expected paths
        • Algorithm: Compares file hashes (MD5/SHA) to match same file at new location
        • Updates: Modifies track_locations table to point to new path
        • Returns: int (number of relocated tracks)
    • Batch operations: Optimized multi-track processing
      • Wrapped in SQL transactions for atomicity
      • Prepared statements for performance (avoid SQL parsing overhead)
      • Bulk INSERT for library import (1000x faster than individual inserts)
    • Track relocation: Handles moved/renamed files
      • Compares file hashes to detect same file at new path
      • Updates track_locations table with new path
      • Preserves all metadata, cues, play history, etc.
    • Metadata sync: Coordinates between database and audio files
      • Read: Database → Track object → UI
      • Write: UI → Track object → Database → Audio file tags
      • Conflict resolution: User preference for DB vs. file tags
  • CueDAO (.cpp): Cue point persistence and management

    • Cue types: Supports multiple cue point categories
      • Main cue: Single default start position (type = CUE)
      • 36 hotcues: Instant recall points (type = HOTCUE, index 1-36)
      • Intro/outro markers: Auto-DJ transition points (type = INTRO_START, etc.)
      • Saved loops: Named loops with start/end (type = LOOP, has length)
    • Operations: Full CRUD for cue points
      • saveCue(Cue* pCue): INSERT or UPDATE individual cue
        • Parameters: pCue is mutable cue object with position, color, label
        • Returns: bool (true on success)
        • ID assignment: Sets id field on INSERT for new cues (auto-increment primary key)
        • Thread: Must run on Library Thread (SQLite connection constraint)
      • deleteCue(Cue* pCue): Remove cue, update track's cue list
        • Parameters: pCue must have valid database ID
        • Returns: bool (false if cue not in database)
        • Side effect: Removes from track's in-memory cue collection via signal
      • getCuesForTrack(TrackId trackId): Load all cues for track (on track load)
        • Parameters: trackId is database primary key (Strongly-Typed IDs)
        • Returns: QList<CuePointer> (empty list if no cues)
        • Sorting: Returns cues ordered by position (earliest first)
        • Called: During Track::load() to populate cue collection
      • getCuesByType(TrackId trackId, mixxx::CueType type): Filter by cue category
        • Parameters: type filters for specific cue category (enum)
        • Returns: Filtered QList<CuePointer>
        • Use case: Get only hotcues, or only intro/outro markers
    • Hotcue management: 36 hotcues per deck implementation
      • Labels: Text stored in label column
      • Colors: RGB integer stored in color column
      • Positions: Frame position stored in position column (FramePos)
      • Persistence: Cues survive track eject, restored on reload
    • Database schema: cues table structure
      • Foreign key to track_id (cascading delete)
      • Indexed on track_id for fast track cue lookups
      • type enum for cue category filtering
  • PlaylistDAO (.cpp): Playlist and crate operations

    • Playlist operations: User-created playlists
      • createPlaylist(const QString& name, const HiddenType hidden): New playlist with auto-generated ID
        • Parameters: name is user-visible playlist name, hidden controls library view visibility
        • Returns: PlaylistId (new playlist database ID, Strongly-Typed IDs)
        • Default values: Sets date_created, date_modified to current timestamp
        • Signal: Emits playlistCreated() to update UI (Signal/Slot Connection Patterns)
      • addTracksToPlaylist(PlaylistId playlistId, const QList<TrackId>& trackIds): Append tracks
        • Parameters: playlistId is target playlist, trackIds are tracks to append
        • Returns: int (number of tracks successfully added)
        • Position assignment: Automatically assigns next available positions (N+1, N+2, ...)
        • Duplicates: Allows same track multiple times (playlists vs. crates)
      • insertTrackIntoPlaylist(TrackId trackId, PlaylistId playlistId, int position): Insert at index
        • Parameters: position is 0-based insertion index
        • Returns: bool (false if position out of range)
        • Reordering: Increments positions of all tracks >= insertion point
        • Transaction: Uses SQL transaction to ensure atomic position updates
      • removeTracksFromPlaylist(PlaylistId playlistId, const QList<int>& positions): Remove by position
        • Parameters: positions are 0-based track indexes to remove
        • Returns: int (number of tracks removed)
        • Gap closing: Decrements positions of tracks after removed items
      • getTracksInPlaylist(PlaylistId playlistId): Ordered track list
        • Returns: QList<TrackId> ordered by position column
        • Performance: Single query with ORDER BY position
        • Used by: Table models for displaying playlist contents (Qt Model/View)
    • Crate operations: Tag-like track grouping
      • Crates are unordered collections (vs. playlists which are ordered)
      • Supports drag-and-drop from multiple tracks
      • Used for genre, mood, or custom categorization
    • Track ordering: Playlist position management
      • position column maintains order (1, 2, 3, ...)
      • Reordering updates all affected positions in transaction
      • Optimized to minimize database writes
    • Metadata: Playlist properties
      • Name, description, date created, date modified
      • Locked status (prevent accidental modification)
      • Hidden status (library view filtering)
  • SQL encapsulation: Each DAO wraps one entity type, isolates schema knowledge (Database Schema)

    • Benefits: Schema changes localized to single DAO class
    • Parameterized queries: All queries use prepared statements (prevents SQL injection)
    • Error handling: DAOs return error codes, callers handle failures
    • Transaction management: DAOs provide transaction helpers for multi-operation atomicity
  • Testing: Enables mocking for unit tests without real database (Testing with Mock Managers)

    • Mock DAOs: Test implementations return hard-coded data
    • In-memory database: SQLite :memory: for integration tests
    • Isolated tests: Each test gets fresh database, no cross-test contamination
  • Thread restriction: All DAO methods run in library thread only (Library Thread)

    • Reason: SQLite connection not thread-safe (one connection per thread)
    • Cross-thread access: Use signals to queue operations to library thread
    • Pattern: GUI thread emits signal → library thread handles → signal back with result
  • Track (.cpp) / TrackPointer - QSharedPointer<Track>

    • Reference-counted: Automatic deletion when last pointer released (Qt Smart Pointers, RAII)
    • Thread-safe: Atomic refcount increment/decrement (Thread Safety Considerations)
    • Never use raw pointers: Always use TrackPointer, never Track* (Memory Management)
    • TrackWeakPointer: QWeakPointer<Track> for non-owning references (cache registry)
    • Usage pattern: Pass by value (cheap atomic operation), store as member, return from functions

Qt Model/View (Skin/UI):

  • QAbstractItemModel: Provides data to QTableView widgets (QtWidgets, model/view)
  • Model types: BaseTrackTableModel (base), PlaylistTableModel, BrowseTableModel, LibraryTableModel
  • Operations: Run SQL queries via DAOs (DAO Pattern), cache results, emit dataChanged() signals (Signal/Slot Connection Patterns)
  • Features: Drag-and-drop track reordering, multi-column sort, search filtering, lazy loading for large datasets

SQLite Database (Configuration Files, Core Libraries):

Pattern: All track access via TrackPointer (Qt Smart Pointers). One canonical Track object per file (Singleton, GlobalTrackCache). DAOs handle all SQL (DAO Pattern).

Subsystem Integration (how Library interacts with rest of Mixxx):

  • Data management via SQLite database (Database Schema):
    • Schema: library, track_locations, cues, playlists, crates, playlist_tracks
    • Transactions: All multi-row operations wrapped in BEGIN/COMMIT
    • Indexes: B-tree indexes on location, artist, album, bpm, key, datetime_added
      • Performance: Indexed query is O(log n), full scan is O(n)
      • Example: Finding tracks by BPM with index: ~1ms; without: ~100ms for 10k tracks
    • Full-text search: FTS5 virtual table for searching artist/title/album
    • Versioning: Schema version tracked in settings table, migrations on startup
    • VACUUM: Automatic database compaction when file size exceeds threshold
  • Threading on dedicated Library Thread:
    • Reason: SQLite operations can take 1-100ms (disk I/O, query planning)
    • Pattern: All DAO methods must be called from Library thread
    • Cross-thread: GUI emits signal → Library thread slot → processes → signals back
    • Async: Track loading doesn't block GUI or audio
    • Mutex: SQLite connection protected by QMutex (safe across threads)
    • Connection pool: One connection per thread (SQLite limitation)
  • Engine integration via TrackPointer and GlobalTrackCache:
    • Loading workflow: User drags track → Deck::slotLoadTrack(TrackPointer) called
      • Step 1: GUI thread calls TrackCollection::getTrackByRef(location)
      • Step 2: Library thread queries database or creates new Track
      • Step 3: Returns TrackPointer (QSharedPointer) to GUI
      • Step 4: GUI queues signal to audio thread with TrackPointer
      • Step 5: Audio thread calls EngineBuffer::loadTrack(TrackPointer)
      • Step 6: CachingReader opens file, starts pre-buffering
    • Shared ownership: GUI, Engine, and Library all hold TrackPointer
    • Lifetime: Track stays in memory until all pointers released
    • Thread safety: Track methods protected by internal QMutex
  • UI updates via Skin/UI model/view architecture:
    • Models: LibraryTableModel, PlaylistTableModel, BrowseTableModel
    • Base class: All inherit from QAbstractItemModel (Qt Integration Patterns)
    • Signals: Models emit dataChanged() when track metadata updated
      • Granularity: Only modified rows refreshed (not entire table)
      • Batching: Multiple changes batched into single signal
    • Caching: BaseTrackCache caches last ~1000 visible rows
      • Cache key: Track ID + column number
      • Invalidation: On metadata change, only affected entries cleared
    • Lazy loading: Rows loaded on-demand as user scrolls
    • Threading: Model signals queued from Library thread to GUI thread
  • File monitoring via LibraryScanner (Library Thread):
    • Startup scan: Checks all library directories for changes
    • Trigger: Manual rescan, directory watch events, startup
    • Watch APIs: inotify (Linux), FSEvents (macOS), ReadDirectoryChangesW (Windows)
    • Scan speed: ~10,000 tracks/minute on SSD, ~1,000/minute on HDD
    • Hash checking: MD5/SHA1 of file to detect moves (optional)
    • New tracks: Automatically analyzed in background (Track Analysis Pipeline)
    • Missing tracks: Marked as broken, can be purged or relocated
  • Analysis data for Engine playback:
    • Beat grids: Protocol Buffer binary blob → Beats object
      • Format: Compressed beat positions for entire track
      • Storage: library.beats_version, library.beats_data columns
      • Loading: Deserialized when track loaded into deck
    • Waveform summary: Pre-computed amplitude data for visualization
      • Format: Downsampled to 2 pixels per second (960 points for 8 minute track)
      • Storage: library.waveform BLOB column (compressed)
      • Rendering: OpenGL shader renders from this data at 60fps
    • Key detection: Musical key for harmonic mixing (Camelot Wheel)
    • ReplayGain: EBU R128 loudness normalization (-23 LUFS target)
  • Thread-safe caching via GlobalTrackCache singleton:
    • Pattern: Single canonical Track object per file path
    • Lookup: O(1) hash map access (QHash<QString, QWeakPointer>)
    • Mutex: QMutex protects cache map during insert/remove
    • Lock duration: Held only during map operations (~100ns)
    • Cache size: Unbounded (weak pointers), tracks cleaned when last ref dropped
    • Consistency: All subsystems see same Track object → no stale data

For comprehensive architecture details including DAO patterns, track pointers, and global track cache, see Library and Database Architecture. Threading model specifics including background scanning and database operations are covered in Library Thread. Practical examples of proper TrackPointer usage are demonstrated in Example 3: Thread-Safe Library Access. The complete database schema is documented at Database Schema.

History (Library and Database Architecture, DAO Pattern, CHANGELOG.md):

  • Pre-1.0 (2001-2005): iTunes XML parsing only, no internal database
  • 1.x series (2006-2011): SQLite database introduced, basic metadata storage, playlist support
  • 1.10 (2013): GlobalTrackCache introduced to prevent duplicate Track objects
  • 1.11 (2014): Full-text search (FTS3), improved library scanning performance
  • 2.0 (2015-2017): Major refactoring to weak pointer cache, TrackPointer pattern standardized
  • 2.1-2.3 (2018-2020): Database schema migrations, improved track relocation, external library sync
  • 2.4 (2024): Enhanced search syntax, improved crate/playlist performance, database optimization
  • 2.5 (2024-12): Key notation parsing improvements, library scan summaries, BPM lock support
  • 2.6.0 (main branch): Key color coding, overview waveform column, advanced search filters
    • Key color coding: Visual color palette for musical key column (Camelot wheel colors)
    • Overview waveform column: Small waveform preview directly in track table
    • BPM lock filter: Search tracks with locked BPM using bpm:locked query syntax
    • Playlist bulk operations: "Unlock all" and "Delete all unlocked" actions

Controllers (src/controllers/)

The Controllers subsystem enables hardware integration with MIDI and HID devices through XML mappings (MIDI Controller Mapping) and JavaScript scripting (MIDI Scripting, QJSEngine). It provides bidirectional communication—hardware inputs trigger Mixxx actions, while Mixxx state changes update controller LEDs/displays. The scripting layer uses Qt's JavaScript engine (Core Libraries) to allow complex programmable behavior without C++ code changes.

Why This Matters: Controllers demonstrate Mixxx's extensibility (Extension Points)—new hardware works without recompiling via XML + JavaScript. Understanding the scripting API (engine.getValue(), engine.setValue(), Control System) is essential for creating mappings (Controllers and Scripting). The soft-takeover system prevents parameter jumps when physical knob positions don't match software values (Value Change Request Pattern).

  • ControllerManager (.cpp) - Device lifecycle management

    • Dedicated thread: Runs in separate thread, doesn't block GUI
      • Why: USB I/O can take 1-10ms—would freeze UI if run on GUI thread
      • Relationship: Created by CoreServices, manages all Controller instances
    • Enumerators: MidiEnumerator, HidEnumerator
      • Job: Scan for connected USB/MIDI devices every second
      • Platform-specific: Uses PortMidi (Linux), RtMidi (macOS), hidapi (HID)
    • Hot-plug detection: Polling (default 1000ms interval) for USB device changes
      • Example: Plug in Launchpad—within 1 second, Mixxx detects it and loads mapping
      • Can disable for better performance if you don't hot-swap controllers
    • Device identification: USB vendor/product ID → mapping lookup
      • How it works: Scans res/controllers/ and ~/.mixxx/controllers/ for XML files
      • Matches <vendorid> + <productid> in XML to detected USB device
      • Example: Finds Novation-Launchpad-Pro-MK3.midi.xml for vendor 0x1235, product 0x0123
    • Lifecycle: open → load mapping → init script → start polling → runtime → shutdown script → close
      • Each step can fail—manager handles errors gracefully
      • init script sends SysEx to put controller in "Mixxx mode"
      • shutdown script resets LEDs, returns to native mode
    • Multi-controller: Manages multiple simultaneous controllers
      • Can use 2 decks with DDJ-400 PLUS pads with Launchpad—both work independently
    • Settings: Stored per device in ~/.mixxx/controllers/
      • Separate settings for each physical controller (even same model)
  • Controller (.cpp) - Base class for hardware

    • Abstract interface: open()/close() lifecycle, send()/receive() message I/O, loadPreset() mapping (Command Pattern)
    • Subclasses:
    • Input processing: poll() called by manager thread, reads input, dispatches to script callbacks (ScriptEngine)
    • Output methods: LED/motor feedback for bidirectional communication
  • Mapping - XML + JavaScript

    • XML structure: Declarative hardware-to-software binding (MIDI Controller Mapping Format)
    • Device matching: Controller identification via USB properties
      • <vendorid>: USB vendor ID (hex, e.g., 0x1235 for Novation)
      • <productid>: USB product ID (hex, e.g., 0x0036 for Launchpad)
      • <name>: Human-readable device name for UI display
      • Wildcards supported for matching multiple device variants
    • Input mappings: Hardware input → Mixxx control (Command Pattern)
      • MIDI mapping structure:
         <control>
         	<status>0x90</status>    <!-- Note On -->
         	<midino>0x20</midino>    <!-- Note number 32 -->
         	<group>[Channel1]</group> <!-- Target deck -->
         	<key>play</key>          <!-- Target control -->
         	<options>
         		<script-binding/>      <!-- Use JavaScript instead of direct mapping -->
         	</options>
         </control>
      • Direct mapping: MIDI value mapped directly to control (no scripting)
        • Fast: No JavaScript overhead—just direct control.set(value)
        • Limited: 1:1 mapping only—can't do "shift+button" combos
        • Automatic scaling: MIDI 0-127 automatically scaled to control's range
        • Example: Fader on controller directly controls volume—simple and efficient
      • Script mapping: JavaScript function handles input
        • Full flexibility: Can implement shift buttons, long-press, encoder acceleration
        • Conditional: Can read other controls—"if sync is on, do X, else do Y"
        • Multi-control: Single button press can modify multiple controls
        • Example: Shift+Play = Load track (one button, two different meanings)
    • Output mappings: Mixxx control → hardware feedback
      • LED control: Update hardware LEDs based on software state
        • Maps control values to MIDI messages (e.g., play=1 → LED on)
        • Supports multi-color LEDs via velocity values
        • Throttled to prevent USB overflow (max update rate)
      • Display updates: Send text/graphics to controller displays
        • Formatted strings (track name, BPM, time remaining)
        • SysEx for advanced displays (Traktor Kontrol screens)
      • Motor control: Physical feedback (jog wheels, motorized faders)
  • JavaScript layer: Programmable behavior beyond simple mappings (Script API)

    • init(): Controller initialization on device open
      • Send SysEx to configure controller mode
      • Initialize LED states to match current Mixxx state
      • Set up internal JavaScript state (shift button tracking, etc.)
      • Query control values to sync hardware with software
    • shutdown(): Cleanup on device close or Mixxx exit
      • Reset LEDs to default state (don't leave controller lit)
      • Send SysEx to return controller to native mode
      • Clean up timers and connections
      • Save controller-specific state to config
    • Custom input handlers: Complex input processing
      • Shift button state: One button modifies meaning of others
        • Pattern: JavaScript tracks shift button state in variable
        • Example: Button normally = hotcue, shift+button = clear hotcue
      • Multi-button combos: Two buttons pressed simultaneously
        • Technique: Check if button A is already down when button B pressed
      • Long press detection: Button held vs. quick tap
        • How: Use engine.beginTimer()—if button still down after 500ms, it's long press
        • Example: Tap = play from cue; hold = play and continue when released
      • Encoder acceleration: Faster turn = bigger jumps
        • Why: Turn knob slowly = fine control; turn fast = coarse control
        • Technique: Measure time between encoder ticks—smaller time = bigger step
      • Touch-sensitive faders: Different behavior when touched vs. moved
        • Use case: Fader touched = disable auto-sync; moved = manual adjustment
    • LED animation: Visual feedback patterns
      • VU meters: Audio level visualization
        • Map vu_meter_left/vu_meter_right controls to LED brightness
        • Update at ~30Hz (not every frame—USB bandwidth limits)
      • Beat indicators: Flash on beat from beat_active controls
        • Relationship: Connects to ClockControl beat triggers
        • Example: Pad lights up on beat 1 of every bar
      • Progress bars: Track position, loop length, effect wet/dry
        • Visual feedback on parameter changes
      • Blink patterns: Tempo-synced blinking using beginTimer()
        • Example: Sync button blinks at BPM/60 Hz when sync lock active
  • Features: Advanced mapping capabilities

    • <options>: User-configurable settings per controller
      • Exposed in Preferences → Controllers as checkboxes/dropdowns
      • Saved per device (multiple instances can have different settings)
      • Examples: Jog sensitivity, shift button behavior, LED brightness
    • Soft-takeover: Prevents parameter jumps when hardware doesn't match software (SoftTakeoverCtrl)
      • Problem: Hardware knob at 50%, software parameter at 100% causes jump when knob moved
      • Solution: Ignore hardware until it crosses software value (catch-up algorithm)
      • Algorithm details: see Soft-Takeover Algorithm below
      • Configurable: threshold (5% default tolerance), per-control enable/disable
      • Implementation: src/controllers/softtakeover.h
    • <script> includes: Code reuse across mappings
      • components.js: Reusable UI components (decks, mixers, effects)
      • midi-components-0.0.js: MIDI-specific abstractions
      • Enables composition: Build complex mappings from standard parts
      • Reduces code duplication across similar controllers
  • ScriptEngine - Qt JavaScript

    • One per controller: Isolated JavaScript execution environment QJSEngine (QtJsEngine, MIDI Scripting)
      • Isolation benefits: Scripts can't interfere with each other
      • Memory management: Engine deleted when controller closed
      • Performance: Compiled to bytecode on load (faster execution)
      • ECMAScript compatibility: ES5 standard (no ES6 features)
    • Global engine object: Mixxx API injected into JavaScript global scope
    • Control access methods (Control System):
      • engine.getValue(group, key): Read control value (returns double)
        • Parameters: group is control group (e.g., "[Channel1]"), key is control name (e.g., "play")
        • Returns: double (current control value, 0.0 if control doesn't exist)
        • Thread-safety: Atomic read (safe from script thread, Control Access Patterns)
        • Performance: Fast (direct atomic access, no signal/slot overhead ~2ns)
        • Example: var bpm = engine.getValue("[Channel1]", "bpm");
      • engine.setValue(group, key, value): Write control value
        • Parameters: group/key identify control, value is new double value
        • Returns: void
        • Thread-safety: Atomic write (safe from script thread)
        • Triggers: Causes connected slots to fire via Observer Pattern
        • Example: engine.setValue("[Channel1]", "play", 1.0);
        • Triggers valueChanged signal (updates UI, other scripts, hardware)
        • Queued to appropriate thread (GUI controls updated on GUI thread)
    • Signal connection methods (Signal/Slot Connection Patterns):
      • engine.connectControl(group, key, callback): React to control changes
        • Callback invoked: Whenever control value changes
        • Parameters: callback(value, group, key) receives new value
        • Use case: Update LEDs when software state changes
        • Threading: Callback runs on script thread (safe for hardware access)
        • Example: engine.connectControl("[Channel1]", "play", MyScript.onPlayChanged)
      • engine.makeConnection(group, key, callback): Advanced connection with enable/disable
        • Returns connection object with trigger() and disconnect() methods
        • Can temporarily disable without recreating connection
        • Useful for managing many dynamic connections
      • engine.makeUnbindableConnection(group, key, callback): One-way connection
        • Cannot be disconnected (stays active for controller lifetime)
        • Slightly more efficient (no connection object overhead)
        • Use when connection never needs removal
    • Utility methods:
      • engine.log(message): Write to Mixxx console (debugging)
        • Messages appear in console and ~/.mixxx/mixxx.log
        • Automatic string conversion (numbers, booleans logged as text)
        • Timestamped for timing analysis
      • engine.trigger(group, key): Manually emit control signal
        • Useful for programmatic state updates
        • Triggers all connected callbacks
      • engine.beginTimer(milliseconds, callback, oneshot): JavaScript timers
        • Returns timer ID for later cancellation
        • Use cases: LED animations, auto-repeat buttons, timeout detection
        • Threading: Callback runs on script thread
        • Precision: ±5ms accuracy (not real-time, good for LEDs)
        • Example: Beat-synced LED blinking using BPM to calculate interval
      • engine.stopTimer(timerID): Cancel running timer
        • Idempotent (safe to call multiple times)
        • Important: Stop timers in shutdown() to prevent leaks
  • Error handling: Scripts isolated from Mixxx core

    • JavaScript exceptions: Caught and logged, don't crash Mixxx
    • Error messages: Show file name, line number, stack trace
    • Console warnings: Undefined control access, type mismatches
    • Recovery: Script continues after error (allows debugging without restart)
  • Sandboxed execution: Security restrictions for safety

    • No file I/O: Cannot read/write files (prevents malicious scripts)
    • No network: Cannot make HTTP requests or open sockets
    • No system access: Cannot execute shell commands or load native libraries
    • Limited Qt API: Only safe APIs exposed (no QProcess, QFile, etc.)
    • Benefits: User can load community scripts without security risk

Bidirectional Communication (Observer Pattern):

  • Output mappings: Control valueChanged signals → MIDI/HID output (Signal/Slot Connection Patterns)
  • Pattern: control changes → script callback → compute LED state → send MIDI note/CC
  • Features: Button LEDs track play state, VU meters, jog wheel indicators, display updates
  • Soft-takeover: Prevents jumps when physical knob position mismatches software
  • Output throttling: Prevents USB overflow

JavaScript API (MIDI Scripting):

engine.getValue(group, item)              // Read control value
engine.setValue(group, item, value)       // Write control value
engine.connectControl(group, item, callback) // React to control changes

Subsystem Integration (how Controllers interact with rest of Mixxx):

  • Hardware integration via Control System:
    • MIDI mapping: Note On/Off (0x90/0x80), CC (0xB0), SysEx for complex devices
      • Note velocity: 0-127 mapped to control parameter space (0.0-1.0)
      • Example: Note 0x20 velocity 64 → [Channel1],play = 0.5
    • HID mapping: Raw byte arrays from USB reports
      • Report parsing: JavaScript extracts button states from byte positions
      • Bitmasking: (report[2] & 0x01) extracts button 1 from byte 2
      • Example: Launchpad Pro MK3 sends 80-byte HID reports at 1kHz
    • Control lookup: engine.getValue("[Channel1]", "play") → hash map lookup
      • Performance: ~50ns for ConfigKey hash, ~2ns for cached ControlProxy
    • Soft-takeover: Prevents parameter jumps when knob position ≠ software value
      • Algorithm: Ignore input until physical value crosses software value
      • Threshold: Default 5% tolerance window
  • Bidirectional communication:
    • Input path: USB → PortMidi/hidapi → Controller::receive() → script handler → ControlProxy::set()
      • Latency: ~1-5ms from button press to control update
      • Polling rate: MIDI ~1ms, HID up to 1kHz (1ms)
    • Output path: Control valueChanged() signal → Controller::slotControlValueChanged() → script → MIDI/HID output
      • Throttling: LED updates limited to 30Hz to prevent USB overflow
      • Buffering: Output messages queued, sent in batches
    • LED feedback examples:
      • Play button: Connects to [Channel1],play_indicator → updates when playback starts
      • VU meter: Connects to [Channel1],vu_meter → updates at ~30Hz
      • Beat indicator: Connects to [Channel1],beat_active → pulses on beat
  • Threading on dedicated controller thread:
    • Reason: USB I/O can take 1-10ms, would freeze GUI if on main thread
    • Polling: Controller::poll() called every 1-5ms to check for input
    • Thread count: One thread for all controllers (multiplexed I/O)
    • Priority: Lower than audio thread, higher than GUI thread
    • Signal queueing: Controller thread → Qt::QueuedConnection → audio/GUI threads
  • Scripting via QJSEngine (QtJsEngine):
    • Initialization: init() function called when controller opened
      • SysEx setup: Send device-specific initialization sequences
      • LED state: Set initial LED states to match Mixxx state
      • Example: midi.sendSysEx([0xF0, 0x00, 0x20, 0x29, 0x02, 0x0E, 0x0E, 0xF7]);
    • Callback execution: function onButtonPress(channel, control, value, status, group) { ... }
      • Performance: ~1-5µs per callback on modern CPU
      • Context switching: Callbacks run on controller thread
    • Engine API: engine.getValue(), engine.setValue(), engine.connectControl()
      • Implementation: Calls through to ControlDoublePrivate via QJSEngine bridge
    • Global scope: All scripts share global JS context per controller
      • Isolation: Each physical controller gets separate QJSEngine instance
      • Namespace pollution: Use object namespacing: MyController.init = function() {...}
    • Error handling: JS exceptions caught, logged to console, don't crash Mixxx
  • Library access via control values:
    • Track metadata: [Channel1],artist, [Channel1],title, [Channel1],bpm
      • Read-only: Scripts can read but not modify track metadata directly
      • Display: Send text to controller displays via SysEx
    • Playlist navigation: [Playlist],SelectPrevPlaylist, [Playlist],LoadSelectedTrack
  • Engine control via playback controls:
    • Transport: play, cue_default, cue_gotoandplay, eject
    • Loops: beatloop_activate, loop_in, loop_out, reloop_toggle
    • Hotcues: hotcue_X_activate, hotcue_X_clear, hotcue_X_set
    • Sync: sync_enabled, sync_leader, beatsync
    • Tempo: rate, rate_perm_up, rate_perm_down, rate_temp_up
  • Effects manipulation:
    • Chain control: [EffectRack1_EffectUnit1],group_[Channel1]_enable
    • Parameters: [EffectRack1_EffectUnit1_Effect1],parameter1
    • Meta-parameters: [EffectRack1_EffectUnit1],super1 links multiple parameters
    • Example: Map controller knob to reverb decay time
  • UI response via control observation:
    • Pattern: Controller observes same controls as UI widgets
    • Example: Waveform zooms → [Channel1],waveform_zoom changes → LED updates
    • Consistency: Controller always shows same state as UI
  • Command pattern for extensibility:
    • Zero core changes: New controllers added via XML + JS files only
    • Mapping files: Drop XML + JS into ~/.mixxx/controllers/
    • Hot reload: Can reload scripts without restarting Mixxx (Preferences → Reload)
    • Versioning: Mapping files specify Mixxx version compatibility

For implementation details including JavaScript engine internals, script connections, and MIDI/HID message flow, see Controllers and Scripting Details. Complete XML mapping format reference is available at MIDI Controller Mapping File Format, and the JavaScript API is documented in the MIDI Scripting guide. Controller script testing patterns are covered in Controller Testing.


Effects (src/effects/)

The Effects subsystem provides real-time audio processing through chains of DSP effects that run in the audio thread. Effects can be routed via send/return (post-fader) or insert (in-line) modes, with up to 4 effect units each containing 4 effects in series. The system supports both built-in effects and external LV2 plugins, with all parameters exposed as controls for automation and MIDI mapping.

Why This Matters: Effects run in the audio thread with the same real-time constraints as the Engine—no allocations, no locks, no blocking. The Composite pattern allows flexible routing without hardcoded chains. Understanding EffectState is critical for maintaining per-channel memory (reverb tails, delay buffers) without interference between decks.

  • EffectsManager - Central coordinator

    • Manager role: Owns chains and processors
      • Relationship: Created by CoreServices at startup, manages all effect chains
      • One manager for entire application—effects are global resources
    • Default config: 4 effect chains ([EffectRack1_EffectUnit1-4]), 4 slots per chain
      • Total capacity: 4 chains × 4 slots = 16 simultaneous effects maximum
      • Typical use: Chain 1-2 for decks, Chain 3-4 for master or aux
      • Configurable in preferences—can reduce for better performance
    • Backend loaders: Builtin (C++ compiled), LV2 (plugin format)
      • Builtin: ~20 effects compiled into Mixxx—fast, always available
      • LV2: External plugins—flexible but higher CPU overhead
      • Loading: Scans LV2 plugin directories at startup (can take 1-2 seconds)
    • Registry: Available effects with metadata (name, author, parameters)
      • Purpose: UI can display effect list without loading actual effect code
      • Maps effect ID → metadata (display name, description, parameter definitions)
    • Factory: Creates EffectChain instances
      • Pattern: createEffectChain() returns fully configured chain
      • Handles complex initialization (routing, controls, state management)
    • Routing: Manages routing matrix
      • Flexibility: Any chain can process any channel (deck, master, aux)
      • Matrix: chain × channel enables (e.g., Chain 1 affects Deck 1+2)
    • Presets: Handles preset loading/saving
      • Saved to ~/.mixxx/effects.xml—remember effect settings between sessions
    • Controls: group_[Channel1]_enable (per-deck routing), mix (wet/dry), chain enable
      • Example: [EffectRack1_EffectUnit1],group_[Channel1]_enable = "apply unit 1 to deck 1"
  • EffectChain (.cpp) - Serial processing pipeline

    • Composite pattern: Up to 4 effects in series
      • Sequential processing: Output of slot N becomes input of slot N+1
        • Example: Slot 1 (EQ) → Slot 2 (Delay) → Slot 3 (Reverb) → Slot 4 (Filter)
      • Chain flexibility: Can have 0-4 effects active simultaneously
        • Empty slots are skipped—no CPU overhead for unused slots
      • Order matters: Reverb after distortion ≠ distortion after reverb
        • Why: Reverb on distorted signal sounds harsh vs. reverb then distort sounds smooth
        • Common studio mixing rule applies to DJ effects
      • Uniform interface: All slots have identical API regardless of effect type
        • Makes chain code simple—doesn't need to know what effect is loaded
    • Audio flow: Multi-stage signal path (all in audio thread, every ~5-20ms)
    • Stage 1: Input buffer (from deck or sum of enabled deck sends)
      • Send mode: Deck 1 + Deck 2 mixed together (parallel effect)
      • Insert mode: Single deck (serial in signal chain)
    • Stage 2: Effect slot 1 processing (bypass if empty)
      • If slot empty, just copies input to output—zero CPU
    • Stage 3: Effect slot 2 processing (receives slot 1 output)
    • Stage 4: Effect slot 3 processing (receives slot 2 output)
    • Stage 5: Effect slot 4 processing (receives slot 3 output)
    • Stage 6: Mix with dry signal (wet/dry blend via mix control)
      • mix=0: 100% dry (no effect)—use for bypassing without unloading
      • mix=0.5: 50/50 blend—common for reverb/delay
      • mix=1.0: 100% wet (full effect)—use for filters, EQ
    • Stage 7: Output buffer (to master bus or back to deck)
      • Critical: All 7 stages must complete within audio callback time (~5-20ms)
  • Components: Complete effect chain state

    • 4 EffectSlot instances: Each can hold one effect or be empty
      • Dynamic loading: Can change effects without stopping playback
      • CPU cost: Each active effect adds 1-10% CPU depending on complexity
    • Mix control [0-1]: Wet/dry blend (0 = 100% dry, 1 = 100% wet)
      • Linear crossfade: Simple blend between processed and unprocessed
        • Formula: output = dry * (1-mix) + wet * mix
      • Equal power: Optional mode for perceived constant volume
        • Uses √ curve instead of linear—sounds more natural
      • Per-chain: Each chain has independent mix control
        • Can have Chain 1 at 100% wet, Chain 2 at 25% wet simultaneously
    • Insertion mode: Determines routing strategy
      • Send/return: Chain receives sum of enabled deck sends (parallel)
      • Insert: Chain inserted directly in signal path (serial)
    • Enabled state: Global chain enable/disable (bypass without losing settings)
  • Audio thread: Real-time processing requirements (Audio Thread, Real-Time Audio Thread Requirements)

    • process(pInput, pOutput, numSamples): Main processing entry point
    • Lock-free: No mutexes in audio callback (atomics for parameter reads)
    • Constant time: Processing time must not depend on input content
    • Buffer size: Handles 256-2048 sample buffers (adaptive to system)
    • Latency: Zero-latency for most effects (delay/reverb add intentional latency)
  • Super knob: Meta-parameter system for unified control

    • Concept: One knob controls multiple effect parameters simultaneously
    • Link types per parameter: None, linked, linked-left, linked-right, linked-left-right
    • Scaling: Each parameter has independent super knob response curve
    • Use case: One MIDI knob adjusts entire effect chain character
    • Example: Super knob 0-50% increases reverb size, 50-100% adds echo feedback
  • Presets: Effect chain snapshots (Preferences and Settings)

    • Stored data: Effect IDs, parameter values, mix, enabled state for all slots
    • Location: ~/.mixxx/effects.xml (Configuration Files)
    • Quick load: Instant recall of complex multi-effect setups
    • Shareable: Presets can be exported/imported between users
  • EffectSlot (.cpp) - Individual effect instance

    • Container: Holds one effect with per-channel state (Composite Pattern)
      • Effect instance: Reference to loaded EffectProcessor
      • Per-channel EffectState: Isolated state for each audio stream
        • Stereo decks: Each deck has left+right state
        • Master processing: Separate state for master output
        • State isolation: Prevents crosstalk between channels
      • Slot index: 1-4 position in chain (determines processing order)
    • EffectState: Per-channel memory for stateful effects
    • Delay lines: Ring buffers for echo/delay/reverb (up to several seconds)
    • Filter state: IIR filter coefficients and history samples
    • Phase accumulators: LFO position for modulation effects (flanger, phaser)
    • Envelope followers: Track input dynamics for auto-wah, compression
    • Allocated on load: Memory allocated when effect loaded, freed when cleared
    • Thread safety: State only accessed from audio thread (no locks needed)
  • Loading: Effect selection and initialization

    • Effect ID: String identifier (e.g., "org.mixxx.effects.reverb")
    • Manifest lookup: EffectsManager registry provides effect factory
    • Instantiation: Factory creates EffectProcessor instance
    • Parameter discovery: Effect reports available parameters with metadata
    • Control creation: Slot creates ControlObjects for all parameters
  • Parameters: Exposed as controls for external access (Control System, ConfigKey)

    • Control names: [EffectRack1_EffectUnit1_Effect1],parameter1 through parameter8
    • Parameter metadata:
      • Name: Human-readable label (e.g., "Delay Time", "Resonance")
      • Type: Knob (continuous), button (toggle), combo (discrete choices)
      • Range: Minimum, maximum, default values
      • Units: Milliseconds, Hz, dB, percentage, etc.
      • Scaling: Linear, logarithmic, or custom mapping
    • Dynamic range: Some effects have fewer than 8 parameters
    • Unused parameters: Extra parameter controls disabled (no effect)
  • Parameter properties: Control behavior configuration

    • min/max/default: Value range and initial state
    • Type: Determines UI widget and value interpretation
      • Knob: Continuous value, typically displayed as rotary control
      • Button: Boolean on/off, typically toggle button
      • Combo: Discrete choices (e.g., filter type: LP/HP/BP/BR)
    • Link type: Super knob connection mode
      • None: Parameter not affected by super knob
      • Linked: Parameter scales proportionally with super knob
      • Linked-left: Only left channel affected
      • Linked-right: Only right channel affected
  • Button parameters: Toggle-style effect controls

    • Examples: Reverb freeze, delay feedback hold, filter bypass
    • Behavior: Press = 1.0, release = 0.0
    • Momentary: Can be configured for hold-while-pressed
  • Empty slots: Optimization for unused slots (Performance Characteristics)

    • Pass-through: Direct copy (or optimized away entirely)
    • Zero overhead: No DSP processing, no memory allocation
    • Hot-swap: Can load effect without stopping audio
  • EffectProcessor (.cpp) - DSP implementation

    • Strategy pattern: Pluggable effect implementations (Strategy Pattern)
      • Abstract interface: All effects implement processChannel() method
      • Interchangeable: Effects can be swapped without changing chain code
      • Polymorphism: Chain calls virtual method, actual effect processes
    • Builtin effects: C++ implementations in src/effects/backends/builtin/
    • Filters:
      • Butterworth LP/HP/BP/BR (2-pole, 4-pole, 8-pole)
      • Linkwitz-Riley crossover filters
      • State-variable filter with resonance
    • EQ:
      • Parametric (frequency, Q, gain per band)
      • Graphic (fixed frequencies, adjustable gain)
      • Kill switches (instant mute per band)
    • Reverb:
      • Schroeder reverberator (comb + allpass filters)
      • Freeverb algorithm (optimized for DJ use)
      • Adjustable room size, damping, width
    • Echo/Delay:
      • Tempo-synced (1/4, 1/8, 1/16 note delays)
      • Free-running (millisecond-based delay time)
      • Feedback control with self-oscillation limiting
      • Ping-pong stereo mode
    • Flanger:
      • Short delay line with LFO modulation
      • Adjustable depth, speed, feedback, manual offset
      • Stereo phase options (in-phase, out-of-phase)
    • Phaser:
      • Allpass filter cascade with LFO sweep
      • 4-12 stages configurable
      • Adjustable feedback for resonance
    • Bitcrusher:
      • Sample rate reduction (downsampling)
      • Bit depth reduction (quantization)
      • Adjustable wet/dry per parameter
  • processChannel(): Core DSP entry point

    • Signature: processChannel(pInput, pOutput, numSamples, sampleRate, effectState, groupFeatures)
    • Per-channel: Called separately for each audio stream
    • Inputs:
      • pInput: Input buffer (CSAMPLE*, float array)
      • numSamples: Buffer size (typically 256-2048)
      • sampleRate: Current sample rate (44100, 48000, 96000 Hz)
      • effectState: Per-channel memory (delay buffers, filter state)
      • groupFeatures: Deck info (BPM, beat phase for tempo sync)
    • Output: pOutput buffer (can alias pInput for in-place processing)
  • Features: Advanced DSP capabilities

    • Mono/stereo: Effects can process stereo independently or linked
    • Parameter smoothing: Avoid clicks/zippers when parameters change
      • Low-pass filter: 10-50ms time constant on parameter changes
      • Critical for: Filters (cutoff), delays (time), gains (volume)
    • Tempo sync: BPM-aware parameter interpretation (Master Sync and Clock System)
      • Beat-locked delays: Delay time specified in beats, auto-adjusts to tempo
      • Phase-locked LFOs: Flanger/phaser sync to beat grid
      • Example: 1/4 note delay = 60000ms / BPM at 120 BPM = 500ms
  • LV2 backend: External plugin support (Plugin System)

    • LV2 audio plugins: Linux audio plugin standard
    • Loading: Via lilv library (LV2 plugin host)
    • Scanning: Plugins discovered in system LV2 paths on startup
    • Sandboxing: Plugins run in same process (no IPC overhead)
    • Limitations: Must be audio-rate (no MIDI-only plugins)
  • Sample-rate agnostic: Adapts to system audio configuration (Audio Type Conventions)

    • Supported rates: 44100, 48000, 88200, 96000, 176400, 192000 Hz
    • Filter design: Coefficients recalculated when sample rate changes
    • Delay scaling: Delay times adjusted to maintain same duration
    • Performance: Higher sample rates increase CPU load linearly

Routing (Mediator Pattern):

  • Send/return mode: Parallel effect bus architecture
    • Per-deck enable: Each deck has group_[ChannelN]_enable control [0-1] (Control System)
      • Value range 0.0-1.0 controls send amount
      • Multiple decks can send to same chain simultaneously
      • Sends are summed before effect processing
    • Chain input: Sum of (deck_output × send_level) for all enabled decks
    • Chain output: Added to master mix (not returned to individual decks)
    • Use case: Reverb/delay shared across multiple sources (traditional mixing console paradigm)
  • Insert mode: Serial effect in signal path
    • No per-deck control: Chain processes entire signal
    • Master insert: Placed between sum of all channels and master output
    • Destructive: Input signal fully replaced by effect output
    • Use case: Master compression, limiting, stereo enhancement
  • Routing matrix: Configuration in EngineMixer (Engine)
    • 4 effect chains × 4 decks = 16 send controls: [EffectRack1_EffectUnit1],group_[Channel1]_enable through Unit4/Channel4
    • Headphone routing: group_[Headphone]_enable for cue effects
    • Microphone routing: group_[Microphone1]_enable for mic processing
  • Signal flow: Audio path through effects
    • Post-fader send: FX send occurs after deck volume/gain
    • Pre-master: Effects applied before master EQ/limiter
    • Parallel mixing: Send/return chains mixed additively with dry signal
  • Headphone cue: Preview effects before sending to master
    • Configurable: User preference for effects in headphone (Preferences and Settings)
    • Pre-fader option: Cue can include effects independent of master send
    • Use case: Preview effect settings before enabling on master
  • Output: Effect chain result routing
    • Send mode: Chain output summed to master bus (added to dry signal)
    • Insert mode: Chain output replaces input (no dry signal mixing)

Subsystem Integration (how Effects interact with rest of Mixxx):

  • Audio processing in Audio Thread:
    • Real-time constraints: Must complete within buffer time (~5-20ms)
      • Budget: With 4 chains × 4 effects = 16 effects, each gets ~500µs at 11.6ms buffer
    • Lock-free: Uses atomics for parameter reads, no mutexes in process()
    • Pre-allocated buffers: All DSP buffers allocated in constructor
    • EffectState pattern: Per-channel state prevents cross-deck interference
      • Example: Reverb on Deck 1 doesn't bleed into Deck 2
      • Memory: Each channel gets separate delay lines, filter states
  • Routing between Engine and master mix:
    • Signal flow: EngineChannel::process() → send level → EffectChain::process() → sum to master
      • Send/return: output = drySignal + (effectOutput * sendLevel)
      • Insert: output = effectOutput (replaces dry signal)
    • Routing matrix: 4 chains × (4 decks + master + aux) = 28 possible routings
      • Storage: Bit flags in EffectChain::m_channelStatus
      • Check: if (m_channelStatus & (1 << channelIndex)) { process... }
    • Master routing: Effect chains can process master output for mastering effects
      • Use case: Limiter, compressor, stereo enhancement on master
  • Control exposure via Control System (50+ controls per effect):
    • Chain controls: enabled, mix, group_[ChannelN]_enable, super1-3
    • Effect slot controls: loaded, enabled, next_effect, prev_effect
    • Parameter controls: parameter1 through parameter8 (effect-dependent)
      • Range: 0.0-1.0 in parameter space, mapped to effect's native range
      • Example: Reverb decay 0.0-1.0 → 0.1s-10s internally
    • Button controls: button_parameter1 (toggle/momentary mode)
    • Update rate: Parameters read every audio callback (~200 Hz)
    • Atomics: All parameter reads use atomic loads for thread safety
  • UI/controller access:
    • Widget binding: Effect knobs in skin XML bind to parameter controls
    • MIDI mapping: Controllers map CC messages to effect parameters
    • JavaScript: engine.setValue("[EffectRack1_EffectUnit1_Effect1]", "parameter1", 0.5)
    • Sliders: Use logarithmic scaling for parameters like frequency, decay time
  • Preset storage in ~/.mixxx/effects.xml:
    • Format: XML with effect ID, parameters, chain routing
    • Schema:
       <EffectRack>
       	<EffectUnit id="1">
       		<Effect id="org.mixxx.effects.reverb">
       			<parameter id="decay" value="0.75"/>
       		</Effect>
       	</EffectUnit>
       </EffectRack>
    • Save timing: On app shutdown, after preset changes
    • Restore: On startup, before audio engine starts
  • Plugin loading at startup:
    • Builtin effects: Registered in BuiltInBackend::BuiltInBackend() constructor
      • Count: ~20 effects (filter, reverb, echo, flanger, etc.)
      • Load time: <1ms (already compiled into binary)
    • LV2 plugins: Scanned from /usr/lib/lv2, ~/.lv2, platform paths
      • Scanning: lilv_world_load_all() enumerates all LV2 plugins
      • Filtering: Only audio effect plugins with stereo I/O
      • Load time: 1-3 seconds for 100+ plugins
      • Lazy instantiation: Plugin code loaded only when effect added to chain
    • Manifest caching: Plugin metadata cached to speed up subsequent startups
  • Mixer integration via EngineMixer:
    • Insertion point: After channel processing, before master sum
    • Per-channel sends: Each deck has 4 send level controls (0.0-1.0)
    • Chain processing: EngineMixer calls EffectChain::process(pInput, pOutput, numSamples)
    • Bypassing: When chain disabled, process() returns immediately (zero CPU)
  • Tempo sync with deck BPM:
    • Beat distance: ClockControl provides fractional beat position to effects
    • Sync controls: parameter_link_type=LINK_TEMPO makes parameter tempo-aware
    • Examples:
      • Echo delay time synced to 1/4 note (BPM/4)
      • LFO rate synced to bar length (4 beats)
      • Filter sweep synced to 16 beats
    • Implementation: EffectProcessor::processChannel() receives beat distance
  • Composite pattern for flexible routing:
    • Chain → Slot → Processor hierarchy
    • Dynamic configuration: Can change effect order without recompiling
    • Zero config: Empty slots have zero CPU overhead
    • Uniform interface: All effects implement same EffectProcessor::process() signature

For detailed effects architecture including effect chains, routing, and built-in effects, see Effects System Details. Real-time processing requirements and audio thread constraints are covered in Audio Thread and Real-Time Audio Thread Requirements. LV2 plugin development and integration details are in Plugin System.

History (Built-In Effects, LV2 Backend, CHANGELOG.md):

  • Pre-1.0 (2001-2008): No effects system, external processing only
  • 1.x series (2008-2011): Basic flanger effect, limited effect chain support
  • 1.11 (2013-2014): Major effects refactoring, introduced effect chains and routing system
  • 1.12 (2015): LV2 plugin support added via LILV, effect presets introduced
  • 2.0 (2015-2017): Effect chain improvements, QuickEffect (Super knob) introduced
  • 2.1-2.3 (2018-2020): New built-in effects (reverb, compressor, limiter), improved LV2 integration
  • 2.4 (2024): Effect parameter mapping improvements, performance optimizations
  • 2.5 (2024-12): Effect UI enhancements, parameter range fixes
  • 2.6.0 (main branch): Compressor improvements, stem-aware quick effects, bounds fixes
    • Compressor effect improvement: Adjusted Makeup Time constant calculation for more responsive dynamics processing
    • Quick effects on stems: Support for applying quick effects to individual stem channels
    • QuickFX model bounds fix: Prevented out-of-bounds access in quickFX effect model

Mixer (src/mixer/)

The Mixer subsystem manages player instances (decks, samplers, microphone inputs) through the PlayerManager factory (Manager Pattern, Factory Pattern). Each player type has different capabilities—decks provide full DJ features (loops, sync, effects), samplers offer simple triggering, and auxiliary inputs provide pass-through with ducking. Understanding the Mixer is essential for adding new player types or modifying deck behavior.

Why This Matters: The PlayerManager is Mixxx's implementation of the Factory pattern (Key Architectural Patterns)—it's the single source of truth for creating players. Each deck has 100+ controls exposed through the Control System (Control System), making them fully scriptable and MIDI-mappable (Controllers, MIDI Scripting). Understanding the player abstraction helps when adding features that work across all player types (Engine, EngineBuffer).

  • PlayerManager - Player factory and registry

    • Factory pattern: Creates decks, samplers, preview decks
      • Why factory: Consistent initialization—all decks created the same way
      • Single source of truth: Only place that creates players—no hidden deck instances
      • Relationship: Created by CoreServices, provides players to EngineMixer
    • Default configuration: 2 decks, 8 samplers, 1 preview deck
      • Configurable: Can have 4 decks + 64 samplers in preferences
      • Memory cost: Each deck uses ~10MB, each sampler ~5MB
      • CPU cost: Each active player adds 1-5% CPU depending on features used
    • Creates: Deck ([ChannelN]), Sampler ([SamplerN]), PreviewDeck ([PreviewDeckN])
      • Example: [Channel1] = Deck 1, [Sampler3] = Sampler 3
      • Full initialization: Creates EngineBuffer, all controls, UI connections
      • Startup time: ~100-200ms per deck (EngineControl creation)
    • Player components: Each owns EngineChannel, connects to EngineMixer
      • Ownership: PlayerManager owns players, players own EngineChannels
      • Qt object tree: Parent/child relationships for automatic cleanup
    • Lookup methods: Find players by identifier
      • getPlayer(const QString& group): Get player by control group name
        • Parameters: group is control group string (e.g., "[Channel1]", "[Sampler1]")
        • Returns: BaseTrackPlayer* (null if not found)
        • Use case: Controllers/scripts need to find which deck to control
        • Example: Script says "load track to Deck 1"—PlayerManager finds [Channel1]
      • getDeck(unsigned int deck): Get deck by 1-based index
        • Parameters: deck is 1-based deck number (1, 2, 3, 4)
        • Returns: Deck* (null if deck doesn't exist)
        • Bounds: Returns null for deck numbers beyond configured count
      • getSampler(unsigned int sampler): Get sampler by 1-based index
        • Parameters: sampler is 1-based sampler number
        • Returns: Sampler* (null if sampler doesn't exist)
    • Static helpers: Group string parsing utilities
      • static bool isDeckGroup(const QString& group): Check if group is a deck
        • Returns: true for "[Channel1]", "[Channel2]", etc.
        • Pattern: Check without creating object instance
      • static bool isSamplerGroup(const QString& group): Check if group is a sampler
        • Returns: true for "[Sampler1]", "[Sampler2]", etc.
  • Deck (.cpp) - Full-featured player

    • Components: Complete playback system (everything needed for DJ performance)
      • EngineBuffer: Per-deck audio processing pipeline
        • Disk I/O: CachingReader loads audio from files in background thread
        • Time-stretch: EngineBufferScale handles key lock + tempo changes
        • Feature controls: CueControl (hotcues), LoopingControl (loops), BpmControl (sync), etc.
        • Relationship: Created by Deck, processes audio in Engine thread
      • EngineDeck: Audio channel implementation
        • Volume/gain: Master volume + pregain (input trim)
        • 3-band EQ: High/mid/low + kill switches (total mute per band)
        • 4 effects sends: Can send to any of 4 effect chains (parallel processing)
        • Master output: Final signal goes to EngineMixer for summing
        • Relationship: Interfaces between EngineBuffer and EngineMixer
      • Multiple EngineControl instances: Feature implementations
        • Composition pattern: ~12 EngineControls per deck, each handles one feature
        • Processing order: BpmControl → RateControl → CueControl → LoopingControl...
        • Dependency: Early controls affect later ones (rate affects position)
    • 100+ controls per deck: Comprehensive control surface
      • Scale: 2 decks = 200+ controls, 4 decks = 400+ controls
      • Discovery: Use grep "ControlObject" src/engine/controls/ to find them all
      • Purpose: Every deck feature accessible via Control System (UI, controllers, scripts)
    • Transport controls: Basic playback operations (CueControl)
      • play: Toggle playback (ControlPushButton)
        • Type: Boolean control (1.0 = playing, 0.0 = paused)
        • Behavior: Press once to start, press again to pause
        • Handler: CueControl::playPause() toggles state
      • cue_default: Jump to cue point, preview while held (ControlPushButton)
        • CDJ behavior: Press = jump to cue; hold = play from cue, release = return
        • Classic DJ technique: Essential for beatmatching
        • Handler: CueControl::cueDefault() implements preview logic
      • cue_gotoandplay: Jump to cue and start playing (ControlPushButton)
        • One-button start: Useful for samplers, quick deck starts
        • Handler: CueControl::cueGotoAndPlay()
      • cue_gotoandstop: Jump to cue and stop (ControlPushButton)
        • Reset position: Return to beginning without playing
        • Handler: CueControl::cueGotoAndStop()
      • sync_enabled: Enable sync lock (tempo and phase matching)
        • Continuous sync: Stays synced even if BPM changes
        • vs. one-shot: Sync button has momentary (tap) and toggle (hold) modes
      • sync_leader: Become sync leader (others follow this deck)
        • Manual override: Force this deck to be the tempo reference
      • sync_mode: Sync mode (0 = none, 1 = follower, 2 = leader)
        • Read-only: Reports current sync state
      • eject: Unload track
        • Stops playback first, then clears deck
      • LoadSelectedTrack: Load track selected in library
        • Convenience: Controllers can load without drag-and-drop
    • Pitch/tempo controls: Playback rate adjustment (RateControl, BpmControl)
      • rate: Playback rate slider (ControlPotmeter)
        • Range: -1.0 to +1.0 (relative to 100%, affected by rateRange)
        • Example: 0.08 at 8% range = 108% playback speed
        • Handler: RateControl::process() applies rate to playback
      • rate_perm_down/rate_perm_up: Permanent rate adjust buttons (ControlPushButton)
        • Behavior: Nudge rate slider by small increment
        • Step size: Configurable in preferences
      • rate_temp_down/rate_temp_up: Temporary pitch bend for beatmatching (ControlPushButton)
        • Behavior: Hold to temporarily adjust speed, release returns to normal
        • Use case: Align beats when manually beatmatching
        • Handler: RateControl tracks button state, applies temporary offset
      • pitch: Current pitch ratio (ControlObject, read-only)
        • Affected by: Key lock state (can differ from rate)
        • Range: Actual pitch multiplier after key lock processing
      • pitch_adjust: Fine-tune pitch in semitones (ControlPotmeter)
        • Range: Typically ±6 semitones
        • Use case: Harmonic mixing, key correction
      • keylock: Enable key lock to maintain pitch at different tempos (ControlPushButton)
        • Processing: Uses time-stretch algorithm (EngineBufferScale)
        • Quality: RubberBand (high quality) vs SoundTouch (fast)
      • rate_dir: Playback direction (ControlObject)
        • Values: 1 = forward, -1 = reverse
        • Handler: Affects CachingReader read direction
      • rateRange: Rate slider range preset (ControlObject)
        • Values: 0.06 (±6%), 0.08 (±8%), 0.10 (±10%), 0.24 (±24%), 0.50 (±50%)
        • Persistent: Saved in configuration (UserSettings)
    • Loop controls: Loop boundary management (LoopingControl)
      • loop_in/loop_out: Set loop start/end points (ControlPushButton)
        • Handler: LoopingControl::slotLoopIn(), LoopingControl::slotLoopOut()
        • Behavior: Sets boundary at current playback position
        • Quantization: Snaps to beat grid if quantize enabled
      • loop_enabled: Loop on/off toggle (ControlPushButton)
        • Handler: LoopingControl::slotLoopToggle()
        • State: 1.0 when looping active, 0.0 when disabled
      • reloop_toggle: Jump back to last loop (ControlPushButton)
        • Handler: LoopingControl::slotReloopToggle()
        • Behavior: Re-enable and jump to last used loop boundaries
      • beatloop_X_activate: Instant X-beat loops (ControlPushButton, X = 0.03125 to 512)
        • Examples: beatloop_4_activate (4-beat loop), beatloop_0.5_activate (half-beat)
        • Handler: LoopingControl::slotBeatLoop() calculates loop boundaries from BPM
        • Quantization: Always quantized to beat grid (Beat Grids)
      • beatlooproll_X_activate: Temporary rolling loops (ControlPushButton)
        • Behavior: Loop while button held, continue when released
        • Use case: Live remixing, stutter effects
        • Handler: LoopingControl::slotBeatLoopRoll()
      • beatjump_X_forward/_backward: Skip by X beats (ControlPushButton)
        • Examples: beatjump_1_forward, beatjump_16_backward
        • Handler: LoopingControl::slotBeatJump() uses beat grid to jump precisely
        • Maintains: Playback state (keeps playing or paused)
      • loop_double/loop_halve: Adjust loop length (ControlPushButton)
        • Handler: LoopingControl::slotLoopDouble(), LoopingControl::slotLoopHalve()
        • Behavior: Multiplies/divides loop length by 2 (keeps start position)
        • Example: 8-beat loop → double → 16-beat loop
      • loop_move_X_forward/_backward: Move loop by X beats (ControlPushButton)
        • Handler: LoopingControl::slotLoopMove()
        • Behavior: Shifts entire loop (maintains loop length)
        • Quantization: Movement quantized to beat grid
    • Hotcue controls: 36 labeled hotcues (CueControl)
      • hotcue_X_activate: Trigger hotcue X (ControlPushButton, X = 1-36)
        • Behavior: If set, jump to position; if not set, set at current position
        • Handler: CueControl::hotcueActivate() implements dual behavior
        • CDJ mode: Matches Pioneer CDJ hotcue behavior
      • hotcue_X_set: Set hotcue X at current position (ControlPushButton)
        • Behavior: Explicitly set hotcue, overwriting existing if present
        • Handler: CueControl::hotcueSet() creates or updates cue
      • hotcue_X_clear: Delete hotcue X (ControlPushButton)
        • Behavior: Removes hotcue and updates database
        • Handler: CueControl::hotcueClear() deletes via CueDAO
      • hotcue_X_goto: Jump to hotcue X without starting playback (ControlPushButton)
        • Handler: CueControl::hotcueGoto() seeks to position
      • hotcue_X_gotoandplay: Jump and play (ControlPushButton)
        • Handler: CueControl::hotcueGotoAndPlay() seeks + starts playback
      • hotcue_X_gotoandstop: Jump and stop (ControlPushButton)
        • Handler: CueControl::hotcueGotoAndStop() seeks + stops playback
      • hotcue_X_position: Frame position of hotcue X (ControlObject, read-only)
        • Type: Double representing FramePos value
        • Range: 0 to track length in frames, -1 if not set
      • hotcue_X_color: RGB color for hotcue X (ControlObject)
        • Type: Integer encoding RGB (0xRRGGBB format)
        • Persistence: Saved to database via CueDAO
      • hotcue_X_enabled: Whether hotcue X is set (ControlObject, read-only)
        • Type: Boolean (0.0 or 1.0)
        • Use case: Controllers/skins check if hotcue exists before displaying
    • EQ/filter controls: Frequency shaping
      • filterLow/filterMid/filterHigh: EQ gain (-1.0 to +1.0, 0 = neutral)
      • filterLowKill/filterMidKill/filterHighKill: Instant mute toggles
      • filterLow_enabled: Whether low band is killed
      • QuickEffectSuperKnob: Quick effect (filter) amount
    • Effects controls: Effect send amounts (Effects System)
      • [EffectRack1_EffectUnit1],group_[Channel1]_enable: Send amount to chain 1 (0-1)
      • [EffectRack1_EffectUnit2],group_[Channel1]_enable: Send amount to chain 2 (0-1)
      • Similar controls for chains 3 and 4
    • Mixing controls: Volume and positioning
      • pregain: Pre-fader gain (0-4, 1 = unity)
      • volume: Fader position (0-1)
      • pfl: Headphone cue enable (pre-fader listen)
      • orientation: Pan/balance (0 = left, 1 = center, 2 = right)
      • mute: Mute toggle
    • BPM/sync controls: Tempo management (BpmControl)
      • bpm: Current BPM value (ControlObject, read-only)
        • Type: Double (beats per minute)
        • Source: From beat grid analysis (Track Analysis Pipeline) adjusted by rate
        • Update: Recalculated every audio callback when rate changes
      • bpm_tap: Tap tempo button (ControlPushButton)
        • Handler: BpmControl::slotBpmTap() measures time between taps
        • Algorithm: Averages last 4-5 taps to calculate BPM
        • Timeout: Resets if >2 seconds between taps
        • Use case: Manual BPM detection for tracks without beat grids
      • beats_translate_curpos: Align beat grid to current position (ControlPushButton)
        • Handler: BpmControl::slotBeatsTranslate()
        • Behavior: Shifts beat grid so nearest beat aligns with current playhead
        • Maintains: Original BPM, only adjusts phase/offset
      • beats_translate_match_alignment: Align to other deck (ControlPushButton)
        • Handler: BpmControl::slotBeatsTranslateMatchAlignment()
        • Behavior: Matches beat grid phase to sync leader deck
        • Use case: Fix phase drift after manual beatmatching
      • quantize: Quantize enable (ControlPushButton)
        • Handler: BpmControl::slotQuantize()
        • Behavior: When enabled, cue points/loops snap to nearest beat
        • Affects: Hotcue setting, loop boundaries, beatjumps
        • State: 1.0 when enabled, 0.0 when disabled
    • Track info controls: Read-only metadata
      • artist/title/album: Track metadata strings
      • duration: Track length in seconds
      • key: Musical key
      • track_color: Track color from library
      • track_loaded: Whether track is loaded (0 or 1)
  • Features: Advanced playback capabilities

    • Beat grids: Tempo map for sync and quantization (Track Analysis Pipeline)
    • ReplayGain: Automatic volume normalization across tracks
    • Waveforms: Visual representation for beatmatching (Waveform Rendering)
    • Vinyl control: Timecode vinyl support (DVS)
    • Key detection: Harmonic mixing compatibility
  • Sampler (.cpp) - Simplified player

    • Lightweight: BaseTrackPlayer subclass for one-shot samples
      • Less code: ~1/4 the code of a full Deck
      • Less memory: No beat grid cache, simplified waveform
      • Less CPU: Fewer EngineControls to process
    • Has: load track, play/pause, volume, repeat mode
      • Basic transport: Just enough for triggering samples
    • No: pitch control, loops (except saved), sync, EQ (uses master EQ), effects sends
      • Design choice: Samplers are for quick one-shots, not full mixing
      • Workaround: If you need full features, use a 3rd or 4th deck instead
    • Optimized for: short samples, instant triggering, low memory
      • Example: Air horn, siren, vocal drop, drum hit
      • Fast loading: Small samples load in <10ms
    • Velocity-sensitive: MIDI velocity triggering
      • MIDI velocity → volume: Hit pad harder = louder sample
      • Relationship: Controllers can send velocity 0-127, mapped to gain
    • Typical use: sound effects, vocal drops, air horns
    • Banks: Can be grouped for controller mapping
      • Example: Sampler 1-8 on first bank, 9-16 on second bank
      • Shift layers: Same pads trigger different samplers with shift button

Microphone/Auxiliary - Pass-through inputs:

  • Base: Pass-through EngineChannel for external audio (EngineChannel)
  • Microphone: mono/stereo input, talk-over ducking (auto-lowers deck volume when active, configurable threshold/fade via Preferences and Settings)
  • Auxiliary: line-level input (4 available)
  • Both have: input gain, master volume, 3-band EQ, headphone cue, orientation
  • No: track loading or playback controls
  • Latency: Input monitoring delay depends on buffer size (Performance Characteristics)
  • Typical use: live vocals, hardware synths, CDJ inputs

Subsystem Integration (how Mixer/Players interact with rest of Mixxx):

  • Player lifecycle via PlayerManager factory:
    • Creation timing: Players created during CoreServices::initialize()
      • Order: SoundManager → PlayerManager → EffectsManager
      • Reason: PlayerManager needs SoundManager's EngineMixer reference
    • Dynamic creation: Can add/remove decks/samplers in preferences
      • Example: User changes from 2 to 4 decks → PlayerManager::addDeckPlayer()
      • Teardown: Old EngineChannels removed, new ones added
      • Limit: Max 4 decks + 64 samplers (configurable constant)
    • Ownership: PlayerManager owns all Deck/Sampler objects via Qt parent/child
  • Player creation with full initialization:
    • Deck instantiation (complex, 100+ lines):
      1. new Deck(group, pConfig, pEngineChannel, pSoundManager, pEffectsManager)
      2. Creates EngineBuffer with all EngineControl subclasses
      3. Creates 100+ ControlObjects (play, cue, loop, sync, rate, etc.)
      4. Connects signals: trackLoaded(), trackUnloaded(), playPositionChanged()
      5. Registers EngineChannel with EngineMixer
      6. Restores saved state from UserSettings (rate range, quantize, etc.)
    • Sampler instantiation (lightweight, ~20 lines):
      1. new Sampler(group, pConfig, pEngineChannel, pSoundManager)
      2. Simpler EngineBuffer (no sync, loops, analysis)
      3. Fewer controls (~30 vs 100+)
      4. Lower memory footprint (~5MB vs ~10MB per deck)
  • Control exposure per player type:
    • Deck: 100+ controls (play, cue, loop, sync, effects, EQ, etc.)
      • Categories: Transport (8), Tempo (12), Loops (15), Cues (150+), Sync (8), Track info (12)
      • Hotcues: 36 hotcues × 7 controls = 252 controls just for hotcues
    • Sampler: ~30 controls (play, volume, repeat, track info)
      • Simplified: No pitch control, sync, loop controls, effect sends
    • Microphone: ~15 controls (volume, gain, EQ, ducking threshold)
    • Auxiliary: ~15 controls (volume, gain, EQ, passthrough)
  • UI/controller access via control bindings:
    • Skin XML: <PushButton><Connection><ConfigKey>[Channel1],play</ConfigKey></Connection></PushButton>
    • MIDI mapping: <control><group>[Channel1]</group><key>play</key></control>
    • JavaScript: engine.getValue("[Channel1]", "play")
    • Discovery: Use grep -r "ConfigKey(" src/mixer/ to find all controls
  • Library integration for track loading:
    • Workflow: User action → Deck::slotLoadTrack(TrackPointer)EngineBuffer::loadTrack()
      • TrackPointer: QSharedPointer keeps track alive during loading
      • Async: GUI doesn't block while audio file opens
    • Metadata extraction: Artist, title, BPM, key, cues loaded from Track object
    • Analysis restoration: Beat grid, waveform from database
    • Cue restoration: CueControl::trackLoaded() loads hotcues from CueDAO
  • Effects routing (4 sends per deck):
    • Send controls: [EffectRack1_EffectUnit1],group_[Channel1]_enable
    • Send level: 0.0-1.0, controls how much signal sent to effect chain
    • Post-fader: Sends happen after volume/EQ (not pre-fader)
    • Summation: Multiple deck sends to same chain are summed
  • Mixing in EngineMixer::process():
    • Algorithm:
       for (EngineChannel* pChannel : m_channels) {
       	pChannel->process(pBuffer, bufferSize);
       	sum(pBuffer, masterBuffer, bufferSize);  // Simple addition
       }
    • Clipping prevention: Master limiter at end prevents digital clipping
    • Gain structure: Channel → EQ → effects → master volume → limiter
  • Startup coordination:
    • Timing: PlayerManager initialized after SoundManager, before UI
    • Restoration: Loads last session state (loaded tracks, cue positions)
    • Auto-play: Optional auto-play last track on startup
  • Player lookup methods:
    • By group name: PlayerManager::getPlayer("[Channel1]") → returns Deck*
    • By index: PlayerManager::getDeck(0) → returns first deck
    • Iteration: for (Deck* pDeck : getDecks()) for batch operations
    • Type checking: PlayerManager::isDeckGroup("[Channel1]") returns true

For player creation and initialization sequence details, see Application Lifecycle. Practical examples of adding new deck functionality are shown in Example 1: Adding a Simple Deck Feature. Cross-subsystem coordination patterns are demonstrated in Complete Example: Loading a Track.


Skin/UI (src/skin/, src/widget/)

The Skin/UI subsystem provides Mixxx's visual interface through XML-based skin definitions (Creating Skins) that declaratively bind UI widgets to controls (Control System). Skins are hot-swappable without recompiling (Configuration Files), using Qt's widget system (Qt Integration Patterns) for rendering and the Control System for automatic data synchronization (Observer Pattern). Modern skins can also use QML (Qt Framework) for declarative UI with better performance and animations.

Why This Matters: The declarative binding system in skin XML is Mixxx's secret weapon for UI development—widgets automatically update when controls change (Signal/Slot Connection Patterns), and vice versa. No manual signal/slot wiring needed (Qt Integration Patterns). Understanding WBaseWidget (Widget and UI Patterns) and ControlParameterWidgetConnection is essential for adding new widget types (Extension Points). The skin system demonstrates the Observer pattern (Key Architectural Patterns) in action.

  • LegacySkinParser (.cpp) - XML parser (Application Lifecycle)

    • Parses: skin.xml from skin directory (Configuration Files)
      • Location: ~/.mixxx/skins/ (user) or /usr/share/mixxx/skins/ (system, Platform Requirements)
      • Entry point: skin.xml in skin root directory
        • Example: ~/.mixxx/skins/Deere/skin.xml
      • Validation: XML schema validation on parse
      • Error reporting: Line numbers for syntax errors
      • Parse time: ~50-200ms depending on skin complexity (Performance Characteristics)
    • XML to widgets: Element-to-widget mapping (declarative UI, Widget and UI Patterns)
    • <PushButton>WPushButton: Clickable button widget
      • Properties: <ObjectName>, <TooltipId>, <Channel> (ConfigKey), <Connection> (Control System)
      • States: Normal, pressed, highlighted (different images per state)
      • Example: Play button shows ▶ when stopped, ⏸ when playing (Engine)
    • <Knob>WKnob: Rotary knob widget
      • Properties: Angle range (e.g., 300°), number of positions (64 steps)
      • Images: Can use filmstrip (one image with all positions stacked)
      • Interaction: Mouse drag (horizontal/vertical), wheel, or click+drag (GUI Thread)
    • <Waveform>WWaveformViewer: Audio waveform display (Waveform Rendering)
    • <Label>WLabel: Text display (track info, BPM, time)
      • Dynamic: Updates automatically when control changes (Observer Pattern)
      • Example: Shows "Artist - Title" from track metadata (Library)
    • <Number>WNumber: Numeric display with formatting
      • Format string: Can show "120.5 BPM" or "3:45" or "A#m" (Key Detection)
    • <SliderComposed>WSliderComposed: Volume faders, EQ sliders
      • Composed: Handle + rail + background (separate images)
      • Orientation: Horizontal or vertical
    • <Display>WDisplay: Generic value display
    • <VuMeter>WVuMeter: Level meter (peak/RMS, Engine)
  • Features: Advanced skin capabilities

    • <Template>: Reusable component definitions
      • Define once, instantiate multiple times
      • Parameters via <SetVariable> in instances
      • Nested templates supported (templates can use templates)
      • Example: Define deck template, instantiate for Channel1, Channel2, etc.
    • <SetVariable>: Skin variables for parameterization
      • String substitution in property values
      • Enables responsive designs (show/hide based on screen size)
      • Conditional widget creation
    • <Style>: CSS-like styling within skin XML
      • Inline styles vs. external .qss files
      • Property-based selectors
      • Pseudo-states (:hover, :pressed, :checked)
  • Layouts: Widget positioning systems

    • <WidgetGroup>: Container for logical grouping
      • Horizontal/vertical stacking
      • Size policies (fixed, minimum, maximum, expanding)
      • Margins and spacing
    • <SizeAwareLayout>: Responsive layout switching
      • Different layouts for different screen sizes
      • Breakpoints defined in skin XML
      • Enables tablet/desktop variations of same skin
    • Absolute positioning: X/Y coordinates for pixel-perfect layouts
  • Responsive: Context-based property evaluation enables responsive designs

    • Screen size detection: Choose layout based on window dimensions
    • Aspect ratio: Adapt to widescreen vs. square displays
    • Configuration-based: Different layouts for 2-deck vs. 4-deck setup
  • Manifest: Skin metadata in skin.xml attributes (Application Lifecycle)

    • Name: Display name in preferences
    • Author: Skin creator
    • Version: Skin version for compatibility
    • Min Mixxx version: Minimum Mixxx version required
    • Description: Human-readable summary
  • WBaseWidget (.cpp) - Control-aware base class

    • Base class: Mixin for control connections (Observer pattern in action)
      • Not standalone: Mixed into concrete widget classes (WPushButton, WKnob, etc.)
      • Pattern: All widgets inherit from both QWidget AND WBaseWidget
      • Qt integration: Works with Qt's meta-object system
      • Lifecycle: Connections auto-disconnect on widget destruction
        • No leaks: Qt parent/child tree handles cleanup automatically
    • Manages: ControlParameterWidgetConnection instances
      • Purpose: Bridges Control System ↔ Qt Widget world
    • Connection objects: Control ↔ widget binding state
      • Example: Play button ↔ [Channel1],play control
    • Multiple connections: One widget can connect to multiple controls
      • Example: BPM label shows both bpm and bpm_tap (for tap tempo indicator)
    • Bidirectional: Widget → control and control → widget
      • Widget → control: User clicks button → control.set(1)
      • Control → widget: Control changes (from script/sync) → widget updates
      • Automatic: No manual signal/slot wiring—declared in XML
    • Thread-safe: Connections queued across thread boundaries
      • Challenge: Controls change in Engine thread, widgets update in GUI thread
      • Solution: Qt::QueuedConnection—signal queued to GUI event loop
  • Transformation: Parameter/value transformation (Control Behaviors)

    • Parameter space: Control's native range (e.g., -1.0 to 1.0)
    • Value space: Widget's expected range (e.g., 0 to 127 for MIDI)
    • Behavior objects: Pluggable transformation strategies
    • Examples: Linear, logarithmic (volume), rotary (wrapping)
  • Connection side: Split control handling

    • Left/right: For stereo controls (VU meters, balance)
    • Independent: Each side can have different behavior
    • ConfigKey extension: [Channel1],volume_left vs volume_right
  • Methods: Widget connection API

    • onConnectedControlChanged(parameter, value): Override in subclass
      • Called when any connected control changes
      • Receives transformed parameter/value
      • Update widget visual state here
    • getControlParameter(): Read current control value
    • setControlParameter(): Write to control
    • resetControl(): Reset to default value
  • Bidirectional sync: Automatic state synchronization (Signal/Slot Connection Patterns)

    • Widget → control: User clicks button → setControlParameter() → control updated → audio thread sees change
    • Control → widget: Audio thread updates control → signal emitted → onConnectedControlChanged() → widget redraws
    • No polling: Purely event-driven (efficient)

Declarative Binding - XML auto-wiring:

  • XML syntax: Simple connection declarations (Skin XML Reference, Creating Skins)
    • Basic: <Connection><ConfigKey>[Channel1],play</ConfigKey></Connection>
    • Full: ConfigKey + options in single widget definition
    • Multiple: Multiple <Connection> blocks per widget
  • Parser creates: Automatic connection setup (Control Hierarchy)
    • Step 1: ControlProxy created for ConfigKey
    • Step 2: Widget signals connected to control slots
    • Step 3: Control signals connected to widget slots
    • Step 4: Connection registered for cleanup
  • Transformation: Automatic parameter mapping (Control Behaviors)
    • Inferred: Parser determines behavior from control type
    • Explicit: Can specify transformation in XML
    • Caching: Transformed values cached for performance
  • Options: Connection behavior modifiers
    • <EmitOnDownPress>: Emit signal on mouse press (not release)
      • Default: Emit on release (safer for accidental clicks)
      • Use for: Cue buttons (instant feedback)
    • <ButtonState>: Match specific value
      • Only update widget when control equals specified value
      • Use for: Multi-state buttons (sync modes: off/follower/leader)
    • <OnOff>: Binary mapping
      • Map control 0/1 to widget states
      • Use for: Toggle buttons, LEDs
    • <Transform>: Custom value transformation
    • <ConnectValueFromWidget>: Enable widget → control
    • <ConnectValueToWidget>: Enable control → widget
    • Default: Both directions enabled
  • Direction: Control data flow configuration
    • Both (default): Full bidirectional sync
    • Widget only: User input only, no feedback
    • Control only: Display only, no user interaction
    • Use cases: Read-only displays, write-only triggers
  • Multiple: Multiple connections per widget supported (Control System)
    • Example: Button changes play state AND updates LED
    • Coordination: All connections process simultaneously
    • Order: Undefined (connections should be independent)

Themes - Qt stylesheets:

  • Format: XML-based with Qt stylesheets (Qt Integration Patterns)
  • Location: Skin directory contains multiple .qss files (Configuration Files)
  • Selection: User selects theme in preferences (Preferences and Settings)
  • Defines: colors, fonts, backgrounds, borders using CSS-like syntax
  • Example: WLabel[objectName="ArtistLabel"] { color: #FFFFFF; }
  • Features: property selectors, pseudo-states (:hover, :pressed), variables via <Style> element
  • Benefit: Same layout with different visual themes

QML (optional) - Modern declarative UI:

  • Experimental: Compile-time flag MIXXX_USE_QML (Build System)
  • Separate: QML application runs separate from legacy widgets (QtWidgets)
  • Features: declarative bindings, animations, touch gestures, GPU composition
  • Control bridge: QmlControlProxy bridge (Control System)
  • Status: library view implemented, full deck UI in development
  • Benefits: smoother animations (60fps, Performance Characteristics), better HiDPI support, modern component architecture
  • Migration: Gradual migration path alongside legacy skins

Subsystem Integration (how Skin/UI interacts with rest of Mixxx):

  • Visual feedback: Provides visual feedback and user interaction by binding widgets to Control System controls
  • Declarative binding: Uses declarative XML via Observer Pattern
  • Display state: Widgets display state from all subsystems
    • Track metadata from Library (artist, title, BPM)
    • Playback position from Engine (waveforms, time remaining)
    • Effect status from Effects System (parameter values, enabled state)
    • Sync status from Mixer decks
  • User interaction: Button clicks, knob turns write to controls
  • Audio thread propagation: Changes propagate to Audio Thread via lock-free atomics (Thread Safety Considerations)
  • Startup loading: LegacySkinParser loads skin XML at startup (Application Lifecycle)
  • Auto-update: Widget trees auto-update via Qt signals/slots (Signal/Slot Connection Patterns)
  • Thread separation: UI rendering on Main/GUI Thread, audio processing remains real-time and glitch-free (Three Fundamental Threads)
  • Widget hierarchies: Detailed skin system architecture in Widget and UI Patterns shows component organization
  • Skin development: Official guide at Creating Skins covers XML structure, widgets, and best practices
  • GUI threading model: UI event loop and thread safety detailed in GUI Thread explain interaction with other threads
  • Control binding mechanisms: Control System Details shows how widgets connect to controls via ConfigKey

Control System (src/control/)

The Control System is Mixxx's universal communication bus, enabling loose coupling between all subsystems through named controls. Every piece of state—playback position, BPM, loop points, effect parameters—is exposed as a control that any subsystem can read or write. This pub-sub architecture allows skins, controllers, and scripts to integrate without knowing about each other.

Why This Matters: The Control System is why you can map any MIDI controller to any Mixxx function without writing C++. Understanding ControlObject vs. ControlProxy is critical—use ControlObject when you own the control (GUI thread), ControlProxy for lock-free access (audio thread). The global registry means you can access controls from anywhere via string name, but with great power comes responsibility—typos in control names fail silently.

  • ConfigKey - Universal addressing

    • Structure: ConfigKey(QString group, QString item)
      • Internal: Tuple of two QString objects
      • Hashing: Implements qHash() for use in QHash containers
      • Comparison: Implements operator== and operator< for sorting
      • String representation: toString() returns "[group],item"
      • Performance: Hash calculation is ~50ns, comparison is ~10ns
    • Examples:
      • ConfigKey("[Channel1]", "play") - Play button for deck 1
        • Group: Identifies which deck/device/subsystem
        • Item: Specific control within that group
      • ConfigKey("[Channel1]", "hotcue_1_position") - Position of hotcue 1
        • Index in item: Hotcue number embedded in item name
        • Pattern: hotcue_X_ prefix where X is 1-36
      • ConfigKey("[Master]", "crossfader") - Master crossfader position
        • Special groups: [Master] for global controls
      • ConfigKey("[EffectRack1_EffectUnit1]", "mix") - Effect unit wet/dry
        • Hierarchical naming: Rack → Unit → Parameter
    • Naming convention: snake_case for new control items (legacy code may use mixed case)
      • Modern: hotcue_1_position, loop_start_position, beat_active
      • Legacy: playposition, rateRange, reloop_toggle (mixed case)
      • Guideline: New code should use snake_case for consistency
    • Global registry: All controls registered in global ConfigKeyControlDoublePrivate map
      • Implementation: static QHash<ConfigKey, QWeakPointer<ControlDoublePrivate>> s_qCOHash
      • Thread safety: Protected by QMutex during insertion/removal
      • Lookup: O(1) average case, O(n) worst case (hash collision)
      • Size: ~1000 controls in typical Mixxx session with 2 decks
    • String-based lookup: Any subsystem can find controls by string name
      • From JavaScript: engine.getValue("[Channel1]", "play")
      • From skin XML: <Connection><ConfigKey>[Channel1],play</ConfigKey></Connection>
      • From C++: ControlObject::get(ConfigKey("[Channel1]", "play"))
    • Benefits: Enables loose coupling between subsystems, skins, controllers, and scripts
      • Decoupling: Subsystems don't need references to each other
      • Extensibility: New controls added without changing dependent code
      • Scriptability: Everything controllable via string names
      • Introspection: Can enumerate all controls dynamically
    • Drawbacks and gotchas:
      • No compile-time checking: Typos cause runtime failures
        • Mitigation: Use constants like CONTROL_PLAY = "play"
      • Silent failures: Accessing non-existent control returns 0.0
        • Mitigation: Check ControlProxy::isValid() after construction
      • String overhead: QString allocation for each lookup
        • Mitigation: Cache ControlProxy instances
      • Discovery: Hard to find all controls (no IDE autocomplete)
  • ControlObject (.cpp) - Creates and owns controls

    • Ownership semantics: Creates ControlDoublePrivate, registers in global map, emits signals
      • Shared ownership: Multiple ControlProxy instances can reference same ControlDoublePrivate
      • Reference counting: Uses QSharedPointer<ControlDoublePrivate> internally
      • Global registry: QHash<ConfigKey, QWeakPointer<ControlDoublePrivate>> in ControlDoublePrivate::s_qCOHash
      • Weak pointers: Registry uses weak pointers to allow garbage collection
      • Lookup: ControlObject::get(ConfigKey) returns shared pointer to existing control or null
    • Constructor parameters:
       ControlObject(
       		ConfigKey("[Channel1]", "play"), 
       		bool bIgnoreNops = true,     // ignore redundant sets to same value
       		bool bTrack = false,          // enable statistics tracking
       		bool bPersist = false,        // save/restore from user config
       		double defaultValue = 0.0);   // default value
      • bIgnoreNops: Prevents redundant signal emission when value doesn't change
        • Performance: Reduces signal/slot overhead by ~50% in typical usage
        • Use case: Most controls should set this to true
        • Exception: Set to false for controls that need every set() call (e.g., triggers)
      • bTrack: Enables control statistics (get/set counts, timing)
        • Overhead: Adds ~10ns per get/set operation
        • Use case: Debugging performance issues, profiling control usage
        • Access: Statistics available via ControlDoublePrivate::getStats()
      • bPersist: Automatic save/restore from user configuration
        • File: ~/.mixxx/mixxx.cfg (XML format)
        • Timing: Saved on app shutdown, restored on startup
        • Use case: User preferences (rate range, crossfader curve, effect settings)
        • Relationship: See UserSettings for config management
      • defaultValue: Initial value when control is created
        • Restored on load: If bPersist=true and saved value exists, saved value overrides default
    • Thread safety: Safe for access from main/GUI thread via Qt signals and slots
      • Internal storage: QAtomicDouble (atomic read/write via QAtomicInteger<qulonglong>)
      • Signal emission: Queued across thread boundaries via Qt::QueuedConnection
      • Caveat: Don't call methods that emit signals from audio thread (use set() from audio thread is OK)
    • Signals: Emits valueChanged(double) when value changes (Observer Pattern)
      • Connection types: Supports all Qt connection types (Signal/Slot Connection Patterns)
        • Qt::DirectConnection: Same-thread synchronous call
        • Qt::QueuedConnection: Cross-thread queued delivery
        • Qt::AutoConnection: Automatic selection based on thread affinity
      • Performance: ~100ns per connected slot on same thread, ~500ns for queued
      • Observers: Unlimited number of connections supported
    • Lifecycle: Lives on GUI thread, owned by creating object (usually manager or controller)
      • Ownership pattern: Parent owns child via Qt object tree (Qt Integration Patterns)
      • Destruction: Auto-unregisters from global map, disconnects all signals
      • Dangling prevention: QWeakPointer in registry prevents access to deleted controls
    • Persistence: If bPersist=true, value saved to ~/.mixxx/mixxx.cfg (UserSettings)
      • Format: <group name="[Channel1]"><item name="play">1.0</item></group>
      • Atomic writes: Config file written atomically to prevent corruption
      • Backup: Previous config saved as mixxx.cfg.bak before overwriting
    • Parameter transformation: Provides parameter/value transformation via ControlBehavior
      • Behaviors: See Control Behaviors section
      • Types: Clamping (min/max), wrapping (modulo), logarithmic (volume), custom
  • ControlProxy (.cpp) - Lightweight non-owning reference

    • Purpose: Thread-safe access without ownership or lifecycle concerns (Proxy Pattern)
      • Primary use case: Audio thread needs to read/write controls owned by GUI thread
      • Zero ownership: Doesn't keep control alive, only references it
      • Fast: Cached pointer lookup, no hash map access after construction
    • Resolution: Resolves ConfigKey to ControlDoublePrivate pointer once at construction, caches for fast access
      • Lookup: Queries global s_qCOHash map once in constructor
      • Caching: Stores QSharedPointer<ControlDoublePrivate> as member variable
      • Validity: Pointer remains valid as long as ControlObject owner exists
      • Performance: Construction is ~500ns (hash lookup), subsequent operations are ~2ns (cached pointer)
    • Operations: All value operations use atomic QAtomicPointer
       ControlProxy* pControl = new ControlProxy("[Channel1]", "play", this);
       pControl->get();        // thread-safe read (~2ns)
       pControl->set(1.0);     // thread-safe write (~5ns)
       pControl->setAndConfirm(1.0);  // write and return actual value set
       pControl->getParameter();      // get parameter space value (0.0-1.0)
       pControl->setParameter(0.5);   // set in parameter space
      • Performance characteristics:
        • get(): ~2ns (atomic load)
        • set(): ~5ns (atomic store + optional signal emission check)
        • setAndConfirm(): ~10ns (write + read for verification)
        • Comparison: Mutex lock/unlock is ~50ns, so 25x faster
      • Parameter vs Value space:
        • Value space: Native control range (e.g., BPM 60-200)
        • Parameter space: Normalized 0.0-1.0 range
        • Conversion: Handled by ControlBehavior (Control Behaviors)
        • Use case: MIDI controllers work in 0-127, map to parameter space
    • Thread safety guarantees:
      • Lock-free: No mutexes, uses atomic operations exclusively
      • Memory ordering: Sequential consistency (strongest guarantee)
      • Visibility: Changes visible across all threads immediately
      • Audio thread safe: Designed for use in Real-Time Audio Thread
      • Caveat: Don't delete ControlProxy while audio thread might use it
        • Pattern: Create at initialization, delete at shutdown
        • Lifecycle: Typically owned by EngineControl with same lifetime as deck
    • Best practices:
      • Construction: Create once during object initialization, not in audio callback
      • Caching: Store as member variable, not stack variable in process()
      • Validation: Check isValid() after construction (control might not exist)
      • Null checks: get() returns 0.0 if control doesn't exist (safe default)
      • Hot path: Use get()/set() in audio callback, avoid getParameter() (slower)
    • Lock-free: Enables lock-free access from audio thread while GUI modifies control properties
    • Validity tracking: If underlying control deleted, proxy operations become no-ops (no crashes)
    • Thread usage: Audio thread uses ControlProxy for lock-free access (Real-Time Audio Thread Requirements)
    • Benefits: Protects audio thread from GUI control lifecycle changes, eliminates mutexes in timing-critical paths
  • ControlDoublePrivate - Actual control implementation

    • Shared ownership: QSharedPointer<ControlDoublePrivate> ensures lifetime management
    • Atomic operations: Uses atomic operations, safe for real-time audio thread
    • Global registry: Stored in static hash map ConfigKeyQSharedPointer<ControlDoublePrivate>
    • Reference counting: Atomic refcount allows safe concurrent access from multiple threads
    • Signal emission: Emits Qt signals via ControlObject wrapper on GUI thread
    • Value storage: Delegates to ControlValueAtomic<double> for lock-free storage
  • ControlValueAtomic - Lock-free atomic storage

    • Template class: ControlValueAtomic<T> for type-safe atomic values
    • Lock-free implementation: Uses std::atomic<T> for real-time guarantees
    • Memory ordering: Carefully chosen memory orderings for correctness and performance
    • Double values: ControlValueAtomic<double> for most controls
    • Parameter/value duality: Stores both parameter (0-1 normalized) and value (transformed)
    • Compare-and-swap: Supports atomic CAS operations for conditional updates

Control Hierarchy - Layered architecture for thread safety:

ControlObject (QObject wrapper, main/GUI thread safe)
	 ↓
QSharedPointer<ControlDoublePrivate> (actual control implementation)
	 ↓
ControlValueAtomic<double> (lock-free atomic value storage)

Specialized Control Types:

  • ControlPushButton - Momentary buttons

    • CO value represents pressed duration (milliseconds)
    • Automatically resets to 0.0 when released
    • Used for cue buttons, transport controls
    • ButtonPressed behavior: 0.0 = released, >0.0 = pressed
  • ControlIndicator - Output-only status

    • No input allowed (set only, no get from external sources)
    • Used for LEDs, status displays
    • Optimized: Skips signal emission if no listeners
  • ControlPotmeter - Ranged controls

    • Min/max ranges with custom behaviors
    • Center detents for knobs
    • Pluggable ControlBehavior for transformation
    • Examples: Volume faders, EQ knobs, filter knobs
  • ControlBehavior - Parameter transformation

    • Purpose: Map between normalized [0,1] parameter space and physical units (Strategy Pattern)
    • Pluggable strategies:
      • ControlLinBehavior: Linear scaling (default)
      • ControlLogBehavior: Logarithmic curves for volume/EQ (human perception)
      • ControlPotmeterBehavior: Min/max/center positions with different curves per range
      • ControlTTRotaryBehavior: Wrapping behavior for scratch/jog wheels
    • Transformations: MIDI 0-127 ↔ parameter 0.0-1.0 ↔ internal value
    • Example: Volume control uses logarithmic behavior so MIDI value 64 maps to -6dB rather than -12dB
    • Benefits: Changing mapping requires only swapping behavior object, parameter transformation reused

Pattern: All subsystems communicate via controls. Engine writes playback position → control updated → Skin/UI widgets, Controllers scripts, and other subsystems observe changes via signals. Thread-safe atomic access enables lock-free reads from Audio Thread while GUI thread modifies state.

Subsystem Integration (Control System as architectural spine):

  • Engine integration — 100+ controls per deck:
    • Creation: EngineControl subclasses create ControlObjects in constructor
      • Example: CueControl creates hotcue_1_activate through hotcue_36_activate
      • Timing: Controls created during EngineBuffer construction (~50ms)
    • Updates: Audio thread writes control values every callback
      • Position: playposition updated 200 times/sec (512 samples @ 44.1kHz)
      • VU meters: vu_meter_left/right updated every callback
      • Beat indicators: beat_active pulses on beat boundaries
    • Atomics: All control writes use QAtomicDouble::store(value, std::memory_order_release)
      • Memory ordering: Release ensures changes visible to other threads
    • No allocations: Controls pre-created, audio thread only writes values
  • UI binding via WBaseWidget and declarative XML:
    • Connection pattern: Widget creates ControlParameterWidgetConnection
      • Lifecycle: Connection lives as long as widget
      • Auto-disconnect: Qt parent/child tree handles cleanup
    • Signal flow: Control valueChanged(double)Qt::QueuedConnection → widget slot → widget->setValue()
      • Thread crossing: Signal queued from audio → GUI event loop
      • Latency: ~16ms at 60fps (one frame delay)
    • Transformation: ControlBehavior converts parameter ↔ value space
      • Example: Volume control 0.0-1.0 → logarithmic dB scale → knob angle 0°-300°
    • Bidirectional: Widget mouse events → ControlProxy::set() → atomic store
    • XML example:
       <PushButton>
       	<TooltipId>play</TooltipId>
       	<NumberStates>2</NumberStates>
       	<Connection>
       		<ConfigKey>[Channel1],play</ConfigKey>
       	</Connection>
       </PushButton>
  • Controller scripting via QJSEngine bridge:
    • API implementation:
      • engine.getValue(group, item)ControlObject::get(ConfigKey)ControlDoublePrivate::get()
      • engine.setValue(group, item, value)ControlProxy::set(value) → atomic store
    • Performance:
      • getValue(): ~50ns (ConfigKey hash + shared pointer lookup)
      • setValue(): ~100ns (hash + shared pointer + atomic store + signal emit check)
    • Connection:
       var connection = engine.makeConnection("[Channel1]", "play", function(value) {
       	midi.sendShortMsg(0x90, 0x01, value * 127);  // Update LED
       });
    • Signal delivery: Control → Qt::QueuedConnection → script callback on controller thread
    • Error handling: JS exceptions logged, don't crash Mixxx
  • Library metadata exposed as read-only controls:
    • Track info: [ChannelN],artist, title, album, year, genre, comment
    • Analysis: bpm, key, duration, samplerate, bitrate
    • Colors: track_color (RGB value from library)
    • Update: Controls updated when new track loaded via trackLoaded() signal
    • Read-only: UI/controllers can read, only Library can write these controls
  • Effects parameters exposure:
    • Controls per effect: 8 parameters + meta-controls
      • Example: [EffectRack1_EffectUnit1_Effect1],parameter1 through parameter8
    • Dynamic creation: Controls created when effect loaded into slot
    • Destruction: Controls destroyed when effect unloaded
    • Meta-parameters: super1, super2, super3 link multiple parameters
      • Implementation: Meta-parameter changes propagate to linked parameters
  • Persistence via UserSettings:
    • Save timing: On app shutdown, UserSettings::save() writes ~/.mixxx/mixxx.cfg
    • Format: XML with nested groups
       <group name="[Channel1]">
       	<item name="rateRange" value="0.08"/>
       	<item name="quantize" value="1"/>
       </group>
    • Restore timing: On startup, before controls created
    • Matching: ControlObject constructor checks UserSettings for saved value
      • Precedence: Saved value overrides defaultValue parameter
    • Opt-in: Only controls with bPersist=true are saved
      • Example: Rate range persists, playback position doesn't
    • Atomic write: Write to temp file, then atomic rename to prevent corruption

For complete implementation details including ConfigKey addressing, control hierarchies, specialized types, and control behaviors, see Control System Details. Threading model specifics explaining when to use ControlObject vs. ControlProxy are covered in Audio Thread and GUI Thread. Qt integration patterns including QSharedPointer usage in ControlDoublePrivate are detailed in Qt Smart Pointers. Practical examples of creating new controls are shown in Example 1: Adding a Simple Deck Feature.

History:

  • 1.0 (2002): Control System established with ConfigKey addressing - oldest and most fundamental architecture pattern, scaled from ~50 to over 1000 controls
  • 1.6 (2008): ControlObject/ControlProxy separation for thread-safe access
  • 1.8 (2010): ControlBehavior for clamping/wrapping values
  • 1.9 (2011): Atomic operations via QAtomicInteger replaced mutexes - lock-free audio thread reads
  • 1.10 (2012): ControlDoublePrivate with QSharedPointer to prevent dangling pointers
  • 1.11 (2013): bPersist flag formalized for automatic settings persistence

Types and Namespaces

Mixxx uses a type-safe design with strongly-typed wrappers and namespaces to prevent common programming errors like mixing incompatible units (samples vs. frames), confusing database IDs, or performing incorrect time calculations. The mixxx namespace encapsulates domain-specific types, while audio types provide compile-time guarantees about sample format and positioning. This approach catches bugs at compile time rather than runtime, making the codebase more robust and easier to refactor.

Why This Matters: Type safety prevents subtle bugs that only appear at runtime. Mixing samples and frames causes off-by-two errors in audio calculations. Confusing TrackId with PlaylistId causes database corruption. The strongly-typed wrappers make these mistakes impossible—the compiler catches them. Understanding mixxx::audio::FramePos vs. raw integers is essential for Engine work.

The mixxx Namespace

Modern Mixxx code uses the mixxx namespace to avoid polluting the global namespace and to provide clear domain organization. Nested namespaces like mixxx::audio group related types together, making code more readable and preventing naming conflicts with external libraries. This is particularly important in Engine and Library code where precise type semantics are critical.

Namespace Structure:

namespace mixxx {
		// Top-level domain types
		class FileInfo;           // file metadata with platform abstractions
		class TrackRecord;        // database track representation
		
		namespace audio {         // audio-specific nested namespace
				using FramePos = ...;      // position in stereo frame pairs (1 frame = 2 samples)
				using SampleRate = ...;     // samples per second (44100 Hz, 48000 Hz, etc.)
				using ChannelCount = ...;   // number of audio channels (1=mono, 2=stereo)
				using SignalInfo = ...;     // combines sample rate + channel count
		}
		
		namespace library {       // library-specific types
				class TrackRef;        // track reference (location + ID)
		}
		
		namespace skin {          // skin parsing types
				class SkinContext;     // skin variable context
		}
}

How Mixxx uses namespaces:

  • mixxx::audio: Used extensively in Engine (EngineBuffer, CachingReader) for sample-accurate positioning
  • mixxx::library: Used in Library DAOs for type-safe track references
  • mixxx::skin: Used in Skin/UI parser for declarative skin context
  • Local using declarations: using namespace mixxx::audio; in .cpp files for cleaner code (never in headers)

Migration Note: Legacy code (pre-2.3) often doesn't use namespaces, but all new code should follow this pattern. When refactoring old code, gradually introduce namespaces to improve type safety without breaking existing functionality.

Audio Type Conventions

Mixxx's audio types enforce semantic correctness at compile time, preventing bugs like confusing samples (individual L/R values) with frames (stereo pairs), or mixing sample positions with sample counts. These strongly-typed wrappers are critical in the Audio Thread where incorrect calculations can cause clicks, pops, or crashes.

Core Audio Sample Types:

CSAMPLE         // float, normalized to range [-1.0, 1.0] (peak amplitude)
								// Used throughout *[Engine](#engine-srcengine)* for audio processing
								// 32-bit IEEE float allows headroom above 1.0 for internal processing
								// Clipped to [-1.0, 1.0] before output to prevent distortion

SAMPLE          // short int, range [SHRT_MIN, SHRT_MAX] (-32768 to 32767)
								// Legacy 16-bit integer format, rarely used in modern code
								// Conversion: CSAMPLE to SAMPLE involves scaling by 32768

SINT            // ptrdiff_t (64-bit on modern systems), used for array indexing
								// Enables compiler auto-vectorization (SSE/AVX optimizations)
								// Prevents 32-bit overflow on large audio buffers
								// Used in loops: for (SINT i = 0; i < bufferSize; ++i)

Mixxx Domain Types (Strongly-Typed IDs):

mixxx::audio::FramePos          // position in stereo frame pairs (sample-accurate)
																// 1 frame = 2 samples (left + right)
																// Prevents "off by one" errors in stereo processing
																// Used by *[EngineBuffer](#engine-srcengine)* for playback position

mixxx::audio::SampleRate        // samples per second (44100 Hz, 48000 Hz, 96000 Hz)
																// Wraps integer Hz value with unit safety
																// Prevents accidentally using BPM where Hz is expected
																// Used in *[CachingReader](#engine-srcengine)* for format detection

mixxx::Duration                 // time duration with unit safety (seconds, milliseconds)
																// Prevents mixing time units in calculations
																// Used for fade durations, latency compensation

Why strongly-typed wrappers matter:

// WRONG: Easy to make mistakes with raw types
double position = 44100.0;  // Is this frames? Samples? Seconds? Unclear!
double bpm = getSampleRate();  // Compiles but nonsensical

// CORRECT: Types prevent errors
mixxx::audio::FramePos position(22050);  // Clearly 22050 frames
mixxx::audio::SampleRate sampleRate(44100);  // Clearly 44100 Hz
// mixxx::audio::FramePos position = getSampleRate();  // Compile error! Type mismatch.

Strongly-Typed IDs

Mixxx wraps database IDs in distinct types to prevent accidentally using the wrong ID in the wrong context. This is a form of type-driven design where the type system enforces business logic: you cannot pass a TrackId where a CrateId is expected, catching bugs at compile time rather than causing database corruption at runtime.

ID Type Hierarchy:

TrackId         // unique track identifier (primary key in tracks table)
								// Used by *[TrackDAO](#library-srclibrary)* for CRUD operations
								// Immutable once assigned, persists across Mixxx sessions
								// Invalid ID represented by TrackId() or TrackId::kInvalidId

DbId            // generic database ID base class
								// Template for creating new ID types
								// Prevents raw integer IDs from being mixed

CrateId         // playlist crate identifier (primary key in crates table)
								// Used by *[PlaylistDAO](#library-srclibrary)* for playlist management
								// Separate type prevents accidentally deleting wrong database rows

LibraryId       // library table identifier
								// Used for track analysis data storage

How ID types prevent bugs:

// WRONG: Raw integers allow dangerous mistakes
void deleteTrack(int id);     // Is this a TrackId? CrateId? Unclear!
deleteTrack(crateId);         // Compiles! But deletes wrong thing in database

// CORRECT: Type-safe IDs prevent misuse
void deleteTrack(TrackId id); // Signature is clear and type-checked
deleteTrack(crateId);         // Compile error! CrateId cannot convert to TrackId

// Benefits:
// 1. Self-documenting code (types convey intent)
// 2. Compiler catches misuse
// 3. Refactoring is safer (changing ID type breaks dependent code)
// 4. No runtime overhead (zero-cost abstraction)

Usage in Mixxx:

  • Library subsystem uses TrackId for all track operations
  • Engine receives TrackId when loading tracks
  • DAO Pattern methods accept strongly-typed IDs
  • Database schema migrations preserve ID type safety

Qt Meta-Type Registration

Custom C++ types must be registered with Qt's meta-type system to be used in signals/slots, stored in QVariant, or passed across thread boundaries via queued connections. Registration tells Qt how to construct, copy, and destroy these types when marshalling them through Qt's type system. This is essential for Mixxx's Control System and Library where custom types flow between subsystems.

Registration Macros:

// In header file (after class definition):
Q_DECLARE_METATYPE(ConfigKey);   // Registers ConfigKey for Qt's meta-type system
Q_DECLARE_METATYPE(TrackId);     // Registers TrackId for signal/slot usage
Q_DECLARE_METATYPE(mixxx::audio::SampleRate);  // Can register namespaced types

What registration enables:

  1. Signal/slot parameters: void signal(TrackId id) can be used in Qt::QueuedConnection
  2. QVariant storage: Store TrackId in QVariant for generic containers
  3. Qt property system: Expose types to QML or dynamic properties
  4. Thread-safe marshalling: Qt can serialize/deserialize across threads

Real Mixxx examples:

// ConfigKey registration enables control system to use QVariant
Q_DECLARE_METATYPE(ConfigKey);  // Needed for skin variable storage

// TrackId registration enables library signals to cross threads
Q_DECLARE_METATYPE(TrackId);    // Allows TrackId in QueuedConnection signals
signals:
		void trackLoaded(TrackId id);  // Qt can marshal TrackId between threads

// Signal emission works across threads safely:
emit trackLoaded(trackId);  // Queued if sender/receiver in different threads

When registration is needed:

  • Any type used as signal/slot parameter with Qt::QueuedConnection
  • Types stored in QVariant (e.g., skin context variables)
  • Types used in Qt property system (rare in Mixxx)
  • Types passed to QJSEngine for Controllers JavaScript API

Not needed for:

  • Types only used with Qt::DirectConnection (same thread)
  • Plain data types already registered by Qt (int, double, QString, etc.)
  • Types never crossing subsystem boundaries

Related documentation:

History:

  • Pre-1.0 (2001-2008): Raw double for audio positions - numerous off-by-one bugs when mixing samples and frames
  • 1.0 (2002): CSAMPLE typedef for float established
  • 1.10 (2012): Strongly-typed IDs (TrackId, PlaylistId) after database corruption bugs
  • 1.11 (2013): mixxx namespace formalized for domain type organization
  • 2.0 (2017): mixxx::audio::FramePos wrapper during C++11 modernization - caught dozens of bugs
  • 2.1 (2018): SampleRate wrapper preventing BPM/Hz confusion
  • 2.2 (2019): mixxx::Duration type for time calculations

Memory Management

Mixxx follows RAII (RAII) principles where resource ownership is tied to object lifetime. Memory is allocated in constructors and freed in destructors, preventing leaks even during exceptions or early returns. The choice between Qt object trees, smart pointers, and RAII wrappers depends on whether the object is a QObject (use parent/child), has single ownership (std::unique_ptr), or is shared across subsystems (QSharedPointer).

Why This Matters: Memory leaks in the audio thread cause gradual performance degradation. Use-after-free crashes are hard to reproduce. Qt's object tree is elegant but has gotchas—deleting a parent while a child holds a raw pointer causes dangling references. Understanding TrackPointer (QSharedPointer) is mandatory—never use raw Track* pointers. The Rule of Five ensures objects are safe to copy/move without double-frees.

Ownership Patterns

Qt Object Tree

Qt's parent/child object tree is the primary memory management mechanism for QObject-derived classes. When a parent is destroyed, Qt automatically deletes all children recursively, eliminating manual delete calls and preventing memory leaks. This is used extensively in Skin/UI widgets, Controllers managers, and Engine components.

How it works:

auto* parentWidget = new QWidget();  // No parent, caller owns
auto* childWidget = new MyWidget(parentWidget);  // parentWidget is parent
// parentWidget now owns childWidget
// When parentWidget is deleted, childWidget is automatically deleted
// No manual delete needed for childWidget

delete parentWidget;  // Deletes both parentWidget and childWidget

Mixxx usage:

  • UI widgets: All widgets in Skin/UI have parent hierarchy (QMainWindowQWidget children)
  • Controllers: Controller objects are children of ControllerManager (Manager Pattern)
  • Engine components: EngineControl instances owned by EngineBuffer via object tree
  • Settings: ConfigObject children automatically cleaned up

Benefits:

  • No manual memory management (no delete calls)
  • Exception-safe (destructors always run)
  • Hierarchical ownership mirrors UI/component structure

parented_ptr

Mixxx's parented_ptr<T> is a smart pointer that enforces Qt object tree ownership at compile time. It prevents common bugs like forgetting to set a parent or accidentally calling delete on a parented object. This wrapper adds debug assertions and makes ownership explicit in the type system.

Enforced safety:

parented_ptr<MyWidget> pWidget = make_parented<MyWidget>(parent);
// DEBUG_ASSERT ensures parent is not null (catches bugs in debug builds)
// No manual delete allowed (compiler error if you try)
// Ownership is clear from the type

// WRONG: Compiler prevents this
// delete pWidget.get();  // Compile error! Cannot delete parented object

// CORRECT: Parent handles deletion
// pWidget will be deleted when parent is destroyed

When to use parented_ptr:

  • Any QObject that should have a parent
  • UI widgets in Skin/UI
  • Controller objects in Controllers
  • Making ownership explicit in member variables

std::unique_ptr<T> provides single ownership with automatic deletion when the owning scope ends. This is the default choice for non-QObject types that have clear lifetime ownership. Ownership can be transferred via std::move(), making it explicit in the code when ownership changes hands.

Usage pattern:

std::unique_ptr<Data> data = std::make_unique<Data>();  // Data is owned here
// Use data normally
data->process();

// Transfer ownership (move semantics)
auto other = std::move(data);  // ownership transferred to 'other'
// data is now null, other owns the Data object

// Automatic cleanup when 'other' goes out of scope
// No manual delete needed

Mixxx usage:

  • Non-QObject data structures: Analysis results, cue point data, beat grids
  • Engine buffers: Temporary audio buffers in EngineBuffer
  • Parser results: Skin XML parse trees, controller mapping data
  • RAII wrappers: File handles, database transactions, mutex locks

Benefits:

  • Zero runtime overhead (compiler optimizes to raw pointer)
  • Move-only semantics prevent accidental copying
  • Clear ownership transfer via std::move()
  • Works with non-QObject types

QSharedPointer

QSharedPointer<T> provides reference-counted shared ownership with thread-safe atomic reference counting. This is essential for objects accessed from multiple subsystems or threads, particularly Library tracks that are shared between Engine, UI, and Library threads.

Usage pattern:

QSharedPointer<Track> pTrack = Track::newTemporary();  // refcount = 1
// Automatically deleted when last reference is released

auto copy = pTrack;  // refcount = 2 (atomic increment)
// Both 'pTrack' and 'copy' point to same Track object

copy.reset();  // refcount = 1 (atomic decrement)
// Track still alive because pTrack holds reference

pTrack.reset();  // refcount = 0, Track is deleted

Mixxx usage (Qt Smart Pointers):

  • TrackPointer: Typedef for QSharedPointer<Track> in Library

    • Shared between Engine (playback), Library (metadata), UI (display)
    • Thread-safe reference counting prevents use-after-free
    • GlobalTrackCache ensures one Track object per file
  • UserSettingsPointer: Typedef for QSharedPointer<UserSettings>

    • Shared configuration across all subsystems
    • Prevents settings being deleted while in use
  • ControlDoublePrivate: Internal control implementation

    • Shared between ControlObject (owner) and ControlProxy (users)
    • Audio thread safely accesses controls without worrying about GUI thread deleting them

When to use QSharedPointer:

Thread safety:

  • Reference count is atomic (safe to copy between threads)
  • Object access still needs synchronization if mutable
  • Use QWeakPointer in caches to allow cleanup

Rule of Five/Zero

Modern C++ Rule: If a class manages resources (memory, file handles, mutexes), either follow the Rule of Five (define all special members) or the Rule of Zero (define none, use RAII wrappers). Mixxx code prefers Rule of Zero wherever possible, using smart pointers and RAII wrappers instead of manual resource management.

Modern Mixxx code follows C++ best practices:

Rule of Zero (preferred):

class SimpleData {
		std::vector<int> data;
		std::string name;
		// Compiler generates all special member functions
};

Rule of Five (when manual resource management is needed):

class ComplexResource {
		ComplexResource(const ComplexResource&) = delete;
		ComplexResource& operator=(const ComplexResource&) = delete;
		ComplexResource(ComplexResource&&) noexcept;
		ComplexResource& operator=(ComplexResource&&) noexcept;
		~ComplexResource();
};

Deprecated: The DISALLOW_COPY_AND_ASSIGN macro should not be used in new code.

Control System (src/control/, Control System)

ControlObject - Thread-safe control wrapper:

ControlProxy - Lightweight non-owning reference:

ConfigKey - Universal addressing (group, item) tuple:

ControlBehavior - Parameter transformation:

  • Strategy pattern: Pluggable ControlNumericBehavior subclasses (Strategy Pattern, Control Behaviors)
  • Implementations: Linear (direct), logarithmic (volume/gain), toggle (binary), rotary (wrapping)
  • Transformation: MIDI [0-127] ↔ parameter [0-1] ↔ value space mapping
  • Hardware abstraction: Enables hardware-neutral control definitions with device-specific scaling (Controllers)

ControlValueAtomic - Lock-free atomic storage:

UserSettings (.cpp) - Optional persistence:

  • Mechanism: Constructor bPersist=true enables automatic save/restore (Preferences and Settings)
  • Storage location: ~/.mixxx/mixxx.cfg (Configuration Files)
  • Serialization: ConfigValue (double to QString conversion)
  • Typical usage: Crossfader curve, EQ settings, effect parameters, skin state
  • Lifecycle: Load before audio engine starts, save after engine stops (Application Lifecycle)

Pattern: All features expose themselves as controls (Control System). UI reads controls (Skin/UI). Audio writes controls (Engine). Controllers map to controls (Controllers).

Relationship to other subsystems:

  • Architectural spine: Control System provides communication mechanism connecting all subsystems
  • Engine: Creates controls for playback state (Engine)
  • Library: Provides track metadata that controls display (Library)
  • Controllers: Map hardware to control changes (Controllers)
  • Effects: Expose parameters as controls (Effects)
  • Skin/UI: Binds widgets to controls for display and interaction (Skin/UI)
  • Control-centric design: Enables loose coupling via Observer Pattern, subsystems communicate without direct dependencies

For Qt smart pointer details including QSharedPointer, QWeakPointer, and parented_ptr usage, see Qt Smart Pointers. Control system integration showing how controls use smart pointers for shared ownership is covered in Control System. Library patterns mandating TrackPointer (QSharedPointer) usage are detailed in Track Pointer Pattern. Common memory management pitfalls and how to avoid them are cataloged in Anti-Patterns. Modern C++ features including std::unique_ptr, std::shared_ptr, and RAII wrappers are covered in Modern C++ Features.

History:

  • Pre-1.0 (2001-2008): Manual new/delete - inconsistent Qt object tree usage, frequent leaks
  • 1.0 (2002): Qt parent/child object tree available but not systematically used
  • 1.9 (2011): TrackPointer (QSharedPointer<Track>) introduced - watershed moment after dangling pointer crashes
  • 1.10 (2012): Valgrind integration exposed hundreds of leaks - systematic RAII adoption began
  • 2.0 (2017): C++11 smart pointer migration accelerated, parented_ptr<T> enforces Qt ownership, memory pool allocators for audio buffers
  • 2.2 (2019): Rule of Zero becomes standard (compiler-generated special members)

Engine and Audio Processing

This section dives deep into Mixxx's real-time audio processing architecture, explaining the strict constraints of the audio thread, the object hierarchy for audio processing, and the patterns that enable glitch-free playback. Understanding these details is essential for anyone modifying playback features, sync algorithms, or audio effects.

Why This Matters: The audio thread runs at high priority with microsecond precision—any violation of real-time constraints causes audible glitches. A single malloc() in the audio callback can cause a 100ms stall. Understanding lock-free programming, pre-allocation, and the EngineControl pattern is mandatory for audio thread work. These aren't suggestions—they're hard requirements enforced by the laws of real-time systems.

Engine Object Hierarchy

All audio processing objects inherit from base classes:

class EngineObject : public QObject {
		Q_OBJECT
	public:
		// Process audio buffer in-place
		virtual void process(CSAMPLE* pInOut,
												const std::size_t bufferSize) = 0;
};
class EngineControl : public QObject {
		Q_OBJECT
	public:
		// Process control logic for deck features
		virtual void process(const double rate,
												mixxx::audio::FramePos currentPosition,
												const std::size_t bufferSize);
		
		// Provide hints to the read-ahead cache
		virtual void hintReader(gsl::not_null<HintVector*> pHintList);
		
		// Respond to track changes
		virtual void trackLoaded(TrackPointer pNewTrack);
};

Real-Time Audio Thread Requirements

The audio processing callback runs in a high-priority thread with strict requirements to prevent audio glitches (clicks, pops, dropouts). This section is critical for anyone modifying Engine or Effects code.

Critical Rule: The audio callback must complete within the buffer time or audio underruns occur. At 44.1kHz with 512-sample buffer, you have 11.6ms to process all decks, effects, and mixing. Missing this deadline causes audible glitches.

Must (required for glitch-free audio):

  • Be lock-free or use very fast locks:

    • Atomic operations: Use QAtomicInteger, std::atomic (Control System)
      • Performance: Atomic read/write is ~2ns, mutex lock/unlock is ~50ns
      • Memory ordering: Use std::memory_order_relaxed for counters, seq_cst for control values
      • Example: m_currentPosition.store(newPosition, std::memory_order_release);
      • Benefit: 25x faster than mutexes, no blocking
    • Lock-free algorithms: Ring buffers, wait-free queues
      • CachingReader: Lock-free ring buffer for disk I/O (Audio Buffer Processing)
      • Implementation: Single-producer single-consumer (SPSC) queue pattern
      • Atomics used: Head/tail pointers with memory barriers
    • Very fast locks (acceptable only if held <1μs):
      • Spinlocks: Acceptable for <10 iterations before falling back
      • Priority inheritance: Use if mixing with non-RT threads
      • Caveat: Never hold locks while doing I/O or allocations
    • Cross-reference: Thread Safety Considerations, Lock-Free Patterns
  • Avoid memory allocations:

    • Reason: malloc()/new can take 1-10ms if heap fragmented (Performance Characteristics)
      • Heap lock: Most allocators use global mutex
      • Page faults: May trigger OS virtual memory operations
      • Fragmentation: Large allocations require heap traversal
    • Solutions:
      • Pre-allocation: Allocate buffers in constructor, reuse in process()
         class MyControl : public EngineControl {
         	std::vector<CSAMPLE> m_buffer;  // Pre-allocated
         	MyControl() : m_buffer(MAX_BUFFER_SIZE) {}  // Constructor allocates
         	void process(...) {
         		// Reuse m_buffer, don't resize() in process()
         	}
         };
      • Fixed-size containers: Use std::array<T, N> instead of std::vector<T>
      • Memory pools: Pre-allocated object pools for temporary objects
      • Stack allocation: Prefer stack (CSAMPLE buffer[512]) over heap when possible
        • Limit: Stack is typically 1-8MB, don't exceed a few KB per call
    • Detection: Use address sanitizer, runtime checks (Debugging Strategies)
      • Compile flag: -fsanitize=address catches allocations
      • Manual check: assert(!malloc_in_use()); in debug builds
    • Cross-reference: Memory Management, RAII Patterns
  • Complete within the buffer time (typically ~5-20ms):

    • Calculation: bufferTime = bufferSize / sampleRate * 1000ms
      • Example: 512 samples / 44100 Hz × 1000 = 11.6ms
      • Lower bound: 128 samples / 96000 Hz = 1.3ms (extreme low latency)
      • Upper bound: 2048 samples / 44100 Hz = 46.4ms (high latency mode)
    • Budget per deck (with 4 decks, 4 effect chains):
      • Per deck: ~2-3ms available
      • Per effect: ~500μs available
      • Master mix: ~1ms available
      • Total: Must leave headroom for OS scheduling jitter (~20%)
    • Profiling: Use QElapsedTimer to measure callback time (Performance Optimization)
       QElapsedTimer timer;
       timer.start();
       process(buffer, size);
       qint64 elapsed = timer.nsecsElapsed();
       if (elapsed > bufferTimeNs * 0.8) {  // 80% threshold
       		qWarning() << "Audio callback took" << elapsed / 1000 << "μs";
       }
    • Monitoring: SoundManager tracks callback time, logs warnings if >80% budget used
    • Cross-reference: Performance Characteristics, Profiling
  • Use Qt::DirectConnection for control callbacks:

    • Reason: Qt::QueuedConnection queues to event loop\u2014adds frame of latency
      • Delay: Queued signals delivered on next event loop iteration (~16ms at 60fps)
      • Problem: Audio thread needs immediate response to control changes
    • Direct connection: Function called immediately, runs on caller's thread
       connect(pControl, &ControlObject::valueChanged,
       				this, &MyControl::slotValueChanged,
       				Qt::DirectConnection);  // Runs on audio thread!
    • Danger: Slot runs on audio thread\u2014must follow all audio thread rules
      • No GUI calls: Don't touch widgets from slot
      • No blocking: Don't use mutexes in slot
      • Keep fast: Slot execution time counts toward audio callback budget
    • Alternative: Read control value using ControlProxy::get() in process()
      • Trade-off: No signal overhead, but must poll every callback
      • When to use: For frequently-read controls (position, rate)
    • Cross-reference: Signal/Slot Connection Patterns, Control System

Must Not (violating these causes glitches/crashes):

  • Allocate memory (no new, malloc, or container resizing):

    • Checked above: See detailed explanation in "Avoid memory allocations"
    • Container operations that allocate:
      • std::vector::push_back() (may reallocate)
      • std::string::append() (may reallocate)
      • QList::append() (may reallocate)
      • QHash::insert() (may rehash)
    • Safe alternatives:
      • std::vector::operator[] (no allocation if within capacity)
      • std::array::operator[] (fixed size, never allocates)
      • Pre-sized containers: vec.reserve(1000) in constructor
    • Cross-reference: Memory Management
  • Use mutexes (use atomics instead):

    • Problem: Mutex can block if lock held by lower-priority thread
      • Priority inversion: Audio thread (high priority) waits for GUI thread (low priority)
      • Worst case: Unbounded blocking time\u2014guaranteed glitch
    • Example of bad code:
       QMutex m_mutex;
       void process(...) {
       	QMutexLocker lock(&m_mutex);  // WRONG! Can block for 100ms+
       	// ... audio processing ...
       }
    • Correct approach:
       QAtomicInt m_value;
       void process(...) {
       	int value = m_value.load(std::memory_order_acquire);  // Lock-free
       	// ... use value ...
       }
    • Exception: Spinlocks acceptable if held <1μs and rare
    • Cross-reference: Thread Safety, Atomics
  • Perform I/O operations:

    • File I/O: read(), write(), fopen() can take 10-100ms (Performance Characteristics)
      • Disk latency: HDD seek time is ~10ms, SSD is ~0.1ms
      • OS scheduling: File operations may cause thread to sleep
    • Network I/O: send(), recv(), DNS lookups can block indefinitely
    • Device I/O: Accessing /dev or /proc can stall
    • Solution: Use separate I/O thread (CachingReader)
      • Pattern: Producer/consumer with ring buffer
      • Audio thread: Reads from pre-filled buffer (lock-free)
      • I/O thread: Fills buffer from disk (can block)
    • Cross-reference: Audio Buffer Processing, CachingReader
  • Call GUI functions:

    • Problem: GUI toolkit (Qt Widgets) is not thread-safe
      • QPainter: Crashes if called from non-GUI thread
      • QWidget methods: Undefined behavior from audio thread
    • Crash example:
       void process(...) {
       	myWidget->setText("Playing");  // CRASH! Wrong thread!
       }
    • Correct approach: Update control, let GUI thread observe
       void process(...) {
       	m_pPlayIndicator->set(1.0);  // Atomic control update
       	// Widget connected to control updates automatically on GUI thread
       }
    • Cross-reference: Skin/UI, Thread Separation

Performance budget breakdown (512 samples @ 44.1kHz = 11.6ms):

  • Disk I/O (CachingReader): 0ms (separate thread, pre-buffered)
  • Audio decoding: 0ms (pre-decoded in buffer)
  • Time-stretch (RubberBand): 2-4ms per deck (reduced with multithreading in main branch)
  • EQ processing: 0.5-1ms per deck (3-band IIR filters)
  • Engine controls: 0.5ms per deck (cue, loop, rate, bpm, clock)
  • Effects chains: 1-3ms total (4 chains × 4 effects)
  • Mixing/summing: 0.5ms (all channels to master)
  • Master processing: 0.5ms (master EQ, limiter)
  • Overhead: 2ms (OS scheduling, thread switching, cache misses)
  • Total typical: 8-12ms (70-100% of budget)

Debugging audio thread violations:

  • ThreadSanitizer: Detects data races (Debugging Strategies)
     cmake -DCMAKE_CXX_FLAGS="-fsanitize=thread" ..
  • AddressSanitizer: Detects heap allocations in audio thread
     cmake -DCMAKE_CXX_FLAGS="-fsanitize=address" ..
  • Manual instrumentation: Add assertions in debug builds
     void process(...) {
     	DEBUG_ASSERT(isAudioThread());  // Verify running on audio thread
     	DEBUG_ASSERT(!m_mutex.tryLock());  // Verify no locks held
     }
  • Profiling: Use perf, Instruments (macOS), or VTune (Performance Optimization)

Cross-references:

Audio Buffer Processing

sample-by-sample processing is the fundamental pattern for all audio code in mixxx.

EngineBuffer::process() skeleton (src/engine/enginebuffer.cpp):

void EngineBuffer::process(CSAMPLE* pOutput, const int iBufferSize) {
		// iBufferSize is in samples (e.g., 512 for 256 frames stereo)
		// pOutput is pre-allocated output buffer
		
		// step 1: read from cache (disk I/O thread fills this)
		m_pReader->process();
		
		// step 2: get audio from reader
		m_pReader->read(m_pScratchBuffer, iBufferSize);
		
		// step 3: apply rate/pitch (keylock, tempo change)
		m_pScale->process(m_pScratchBuffer, m_pScaleBuffer, 
											iBufferSize, m_rate);
		
		// step 4: apply EQ
		m_pEQ->process(m_pScaleBuffer, m_pEQBuffer, iBufferSize);
		
		// step 5: apply effects (if enabled)
		m_pEffectsProcessor->process(m_pEQBuffer, pOutput, iBufferSize);
		
		// step 6: update position control (for waveform display)
		m_playPos.store(m_currentPosition, std::memory_order_release);
}

stereo interleaved format:

// CSAMPLE buffer layout: [L0, R0, L1, R1, L2, R2, ...]
// iBufferSize = total samples (frames * 2 for stereo)

void processStereo(CSAMPLE* pBuffer, int iBufferSize) {
		const int channelCount = 2;  // stereo
		const int frameCount = iBufferSize / channelCount;
		
		for (int frameIdx = 0; frameIdx < frameCount; ++frameIdx) {
				int sampleIdx = frameIdx * channelCount;
				
				CSAMPLE left = pBuffer[sampleIdx + 0];
				CSAMPLE right = pBuffer[sampleIdx + 1];
				
				// process left/right
				left = processLeft(left);
				right = processRight(right);
				
				// write back
				pBuffer[sampleIdx + 0] = left;
				pBuffer[sampleIdx + 1] = right;
		}
}

common processing patterns:

gain application:

void applyGain(CSAMPLE* pBuffer, int iBufferSize, double gain) {
		for (int i = 0; i < iBufferSize; ++i) {
				pBuffer[i] *= gain;
		}
		// compiler auto-vectorizes this to SIMD on -O2
}

mixing two buffers:

void mixBuffers(CSAMPLE* pDest, const CSAMPLE* pSrc, 
								int iBufferSize, double srcGain) {
		for (int i = 0; i < iBufferSize; ++i) {
				pDest[i] += pSrc[i] * srcGain;
		}
}

clipping/limiting:

void clipBuffer(CSAMPLE* pBuffer, int iBufferSize) {
		constexpr CSAMPLE kMaxAmplitude = 1.0f;
		constexpr CSAMPLE kMinAmplitude = -1.0f;
		
		for (int i = 0; i < iBufferSize; ++i) {
				pBuffer[i] = std::clamp(pBuffer[i], kMinAmplitude, kMaxAmplitude);
		}
}

SIMD optimization hints:

// good: compiler can vectorize (use 4-8 samples at once)
void processGood(CSAMPLE* pBuffer, int iBufferSize) {
		for (int i = 0; i < iBufferSize; ++i) {
				pBuffer[i] = pBuffer[i] * 0.5f + 0.1f;  // simple math
		}
		// gcc/clang generates SSE/AVX instructions automatically
}

// bad: prevents vectorization
void processBad(CSAMPLE* pBuffer, int iBufferSize) {
		for (int i = 0; i < iBufferSize; ++i) {
				pBuffer[i] = expf(pBuffer[i]);  // complex function call
				if (pBuffer[i] > 0.5f) {        // branch inside loop
						pBuffer[i] *= 2.0f;
				}
		}
		// no SIMD, ~4x slower
}

buffer size considerations:

// typical buffer sizes:
// 256 samples = 128 frames stereo @ 48kHz = 2.67ms
// 512 samples = 256 frames stereo @ 48kHz = 5.33ms
// 1024 samples = 512 frames stereo @ 48kHz = 10.67ms

// always process entire buffer in one callback
void process(CSAMPLE* pBuffer, int iBufferSize) {
		DEBUG_ASSERT(iBufferSize > 0);
		DEBUG_ASSERT(iBufferSize % 2 == 0);  // stereo
		
		// process all samples
		for (int i = 0; i < iBufferSize; ++i) {
				// ...
		}
}

Engine Control Pattern

Every deck feature is implemented as an EngineControl subclass:

class CueControl : public EngineControl {
		Q_OBJECT
	public:
		CueControl(QString group, UserSettingsPointer pConfig);
		
		void process(const double rate,
								mixxx::audio::FramePos currentPosition,
								const std::size_t bufferSize) override;
		
		void trackLoaded(TrackPointer pNewTrack) override;
		
	private:
		// Creates ControlObjects like hotcue_X_activate, etc.
};

Composition Pattern

EngineBuffer owns multiple EngineControl instances:

  • CueControl - Hotcues and main cue point
  • LoopingControl - Loops and beatjumps
  • BpmControl - BPM detection and sync
  • RateControl - Playback rate and pitch
  • ClockControl - Beat timing and fractional tempo triggers

All process() methods are called during each audio callback.

Master Sync and Clock System

EngineSync coordinates tempo and phase alignment across decks for harmonic mixing.

sync modes:

  • none: independent playback, no synchronization
  • follower: matches tempo to master clock, adjusts phase to align beats
  • leader: becomes master clock, other decks follow this deck's tempo/phase

master sync algorithm (src/engine/sync/enginesync.cpp):

1. beat grid alignment:

// calculate current beat distance (0.0 = on beat, 0.5 = halfway between beats)
double calculateBeatDistance(mixxx::audio::FramePos currentPos, 
														 const mixxx::BeatsPointer& pBeats) {
		if (!pBeats) return 0.0;
		
		// find closest beat positions
		auto prevBeat = pBeats->findPrevBeat(currentPos);
		auto nextBeat = pBeats->findNextBeat(currentPos);
		
		if (!prevBeat.isValid() || !nextBeat.isValid()) return 0.0;
		
		// calculate fractional position between beats
		double beatLength = (nextBeat - prevBeat).value();
		double distanceFromPrev = (currentPos - prevBeat).value();
		
		return distanceFromPrev / beatLength;  // 0.0 to 1.0
}

2. tempo adjustment:

void SyncControl::adjustSyncTempo(double targetBpm) {
		double currentBpm = m_pBpm->get();
		
		if (currentBpm == 0.0 || targetBpm == 0.0) {
				return;  // no valid BPM
		}
		
		// calculate rate adjustment needed
		double rateAdjust = targetBpm / currentBpm;
		
		// apply to rate slider (preserves key lock if enabled)
		double currentRate = m_pRate->get();
		double newRate = currentRate * rateAdjust;
		
		// clamp to ±100% range
		newRate = std::clamp(newRate, -1.0, 1.0);
		
		m_pRate->set(newRate);
}

3. phase offset correction:

void SyncControl::adjustSyncPhase(double leaderBeatDistance) {
		double followerBeatDistance = calculateBeatDistance(
				m_currentPosition, m_pBeats);
		
		// calculate phase difference
		double phaseDiff = leaderBeatDistance - followerBeatDistance;
		
		// wrap to [-0.5, 0.5] range (shortest path)
		if (phaseDiff > 0.5) phaseDiff -= 1.0;
		if (phaseDiff < -0.5) phaseDiff += 1.0;
		
		// convert to samples
		double beatLength = 60.0 / m_pBpm->get() * m_sampleRate * 2;  // stereo
		double sampleOffset = phaseDiff * beatLength;
		
		// apply small rate adjustment for gradual correction
		// (instant jump would be audible)
		double rateAdjust = sampleOffset / (m_sampleRate * 0.5);  // correct over 0.5s
		rateAdjust = std::clamp(rateAdjust, -0.05, 0.05);  // max 5% adjustment
		
		m_pRateAdjust->set(rateAdjust);
}

4. leader selection:

class EngineSync {
	private:
		SyncControl* m_pLeader = nullptr;
		QList<SyncControl*> m_followers;
		
	public:
		void updateLeader() {
				// find explicit leader (user pressed sync button while holding shift)
				for (auto* pSync : m_allSyncControls) {
						if (pSync->getSyncMode() == SyncMode::Leader) {
								m_pLeader = pSync;
								return;
						}
				}
				
				// no explicit leader: use "internal" leader
				// (first playing deck, or deck with most recent playback)
				for (auto* pSync : m_allSyncControls) {
						if (pSync->isPlaying()) {
								m_pLeader = pSync;
								return;
						}
				}
				
				m_pLeader = nullptr;  // no leader
		}
		
		void process() {
				if (!m_pLeader) {
						updateLeader();
						return;
				}
				
				// broadcast leader's beat distance to all followers
				double leaderBeatDist = m_pLeader->getBeatDistance();
				double leaderBpm = m_pLeader->getBpm();
				
				for (auto* pFollower : m_followers) {
						if (pFollower == m_pLeader) continue;
						
						// adjust tempo
						pFollower->adjustSyncTempo(leaderBpm);
						
						// adjust phase
						pFollower->adjustSyncPhase(leaderBeatDist);
				}
		}
};

quantization to beats:

// snap cue/loop actions to nearest beat
mixxx::audio::FramePos quantizeToNearestBeat(
				mixxx::audio::FramePos position,
				const mixxx::BeatsPointer& pBeats) {
		
		if (!pBeats) return position;  // no beat grid
		
		auto prevBeat = pBeats->findPrevBeat(position);
		auto nextBeat = pBeats->findNextBeat(position);
		
		if (!prevBeat.isValid()) return nextBeat;
		if (!nextBeat.isValid()) return prevBeat;
		
		// snap to closest
		double distToPrev = (position - prevBeat).value();
		double distToNext = (nextBeat - position).value();
		
		return (distToPrev < distToNext) ? prevBeat : nextBeat;
}

use cases:

  • beatmatching: automatic tempo matching for seamless transitions
  • harmonic mixing: keep tracks phase-aligned throughout sets
  • live remixing: sync loops and samples across multiple decks
  • quantized actions: hotcues/loops snap to beat grid when sync enabled

Engine architecture reference:

History:

  • 1.0 (2002): Basic playback and pitch control established - Engine architecture foundation
  • 1.6 (2008): EngineBuffer and real-time audio thread design - professional-grade audio processing
  • 1.9 (2011): EngineControl pattern formalized for growing deck features
  • 1.11 (2013): Master Sync added with beat distance calculations and leader/follower coordination
  • 2.0 (2017): Migration from SoundTouch to RubberBand - dramatically improved sound quality
  • 2.2 (2019): Lock-free atomics replaced mutexes in audio thread - eliminated priority inversion glitches

Effects System

This section details Mixxx's modular effects architecture, explaining how effects are loaded, routed, and processed in real-time. The system supports both built-in C++ effects and external LV2 plugins through a unified interface, with per-channel state management and flexible routing options.

Why This Matters: Effects run in the audio thread with the same constraints as the Engine. Understanding the Composite pattern (chains containing slots containing processors) is key to extending the effects system. The EffectState pattern enables per-channel memory (reverb tails, delay buffers) without cross-deck interference. Plugin developers need to understand the EffectProcessor interface and real-time guarantees.

Effects Architecture

EffectsManager orchestrates the effects pipeline, managing effect chains, slots, and processor instantiation.

class EffectsManager : public QObject {
		// Owns all effect chains and backends
		void addEffectsBackend(EffectsBackendManagerPointer pBackend);
		EffectChainPointer createEffectChain(const QString& id);
		
	private:
		QMap<QString, EffectsBackendManagerPointer> m_effectsBackends;
		QList<EffectChainPointer> m_effectChains;
};

EffectsBackend abstracts effect plugin formats (built-in, LV2, AudioUnit):

class EffectsBackend {
	public:
		virtual const QList<QString> getEffectIds() const = 0;
		virtual EffectManifestPointer getManifest(const QString& id) const = 0;
		virtual std::unique_ptr<EffectProcessor> createProcessor(
						const EffectManifestPointer pManifest) const = 0;
};

EffectProcessor performs DSP in audio thread:

class EffectProcessor {
	public:
		virtual void process(const ChannelHandle& inputHandle,
												const ChannelHandle& outputHandle,
												const std::size_t numSamples,
												const EffectEnableState enableState,
												const GroupFeatureState& groupFeatures) = 0;
};

Effect Chains and Routing

Signal flow: Audio → EffectChain → EffectSlot → EffectProcessor → Output

Master Output
	├── EffectChain 1 (Reverb → Filter)
	└── EffectChain 2 (Delay → Flanger)

[Channel1] → EffectChain 1 (enabled)
[Channel2] → EffectChain 2 (enabled)

EffectChain controls:

[EffectRack1_EffectUnit1],enabled
[EffectRack1_EffectUnit1],mix
[EffectRack1_EffectUnit1],super1  // Meta-knob controlling multiple parameters
[EffectRack1_EffectUnit1],group_[Channel1]_enable

EffectSlot controls (per effect in chain):

[EffectRack1_EffectUnit1_Effect1],enabled
[EffectRack1_EffectUnit1_Effect1],meta    // Effect meta-parameter
[EffectRack1_EffectUnit1_Effect1],parameter1
[EffectRack1_EffectUnit1_Effect1],parameter2

Built-In Effects

Core effects in src/effects/backends/builtin/:

Effect File Parameters Use Case
Reverb reverbeffect.cpp Decay, damping, bandwidth Add space/depth
Echo echoeffect.cpp Delay time, feedback, ping-pong Rhythmic delays
Filter filtereffect.cpp Cutoff, resonance, type (LP/HP/BP) Frequency sweeps
Flanger flangereffect.cpp Speed, depth, delay Jet/whoosh effects
EQ bessel4lvmixeqeffect.cpp Low/mid/high gains Frequency balancing
Autopan autopaneffect.cpp Rate, depth, smoothing Stereo movement
Tremolo tremoloeffect.cpp Rate, depth, waveform Amplitude modulation
Bitcrusher bitcrushereffect.cpp Bit depth, downsample Lo-fi distortion

Adding New Built-In Effect:

  1. Subclass EffectProcessorImpl<MyEffect> in src/effects/backends/builtin/
  2. Implement processChannel() for DSP
  3. Create EffectManifest describing parameters
  4. Register in BuiltInBackend::getManifests()
  5. Add unit tests in src/test/effects/

Creating a New Effect

complete effect skeleton (src/effects/backends/builtin/myeffect.h):

#ifndef MYEFFECT_H
#define MYEFFECT_H

#include "effects/backends/effectprocessor.h"
#include "engine/effects/engineeffectparameter.h"
#include "util/types.h"

class MyEffectGroupState : public EffectState {
	public:
		MyEffectGroupState(const mixxx::EngineParameters& engineParameters)
				: EffectState(engineParameters),
					m_sampleRate(engineParameters.sampleRate()) {
				// initialize per-channel state (e.g., filter history)
				m_prevSample = 0.0;
		}
		
		~MyEffectGroupState() override = default;
		
		// per-channel persistent state
		double m_prevSample;
		mixxx::audio::SampleRate m_sampleRate;
};

class MyEffect : public EffectProcessorImpl<MyEffectGroupState> {
	public:
		MyEffect() = default;
		~MyEffect() override = default;
		
		// create effect manifest (called once at startup)
		static QString getId() {
				return "org.mixxx.effects.myeffect";
		}
		
		static EffectManifestPointer getManifest() {
				EffectManifestPointer pManifest(new EffectManifest());
				pManifest->setId(getId());
				pManifest->setName(QObject::tr("My Effect"));
				pManifest->setShortName(QObject::tr("MyFX"));
				pManifest->setAuthor("Your Name");
				pManifest->setVersion("1.0");
				pManifest->setDescription(QObject::tr("Does something cool"));
				pManifest->setEffectRampsFromDry(false);  // bypass ramping
				
				// parameter 1: amount (0.0-1.0)
				EffectManifestParameterPointer amount = pManifest->addParameter();
				amount->setId("amount");
				amount->setName(QObject::tr("Amount"));
				amount->setShortName(QObject::tr("Amt"));
				amount->setDescription(QObject::tr("Effect intensity"));
				amount->setValueScaler(EffectManifestParameter::ValueScaler::Linear);
				amount->setUnitsHint(EffectManifestParameter::UnitsHint::Unknown);
				amount->setRange(0.0, 1.0);
				amount->setDefault(0.5);
				amount->setNeutralPointOnScale(0.0);
				
				// parameter 2: frequency (20Hz - 20kHz)
				EffectManifestParameterPointer frequency = pManifest->addParameter();
				frequency->setId("frequency");
				frequency->setName(QObject::tr("Frequency"));
				frequency->setShortName(QObject::tr("Freq"));
				frequency->setDescription(QObject::tr("Center frequency"));
				frequency->setValueScaler(EffectManifestParameter::ValueScaler::Logarithmic);
				frequency->setUnitsHint(EffectManifestParameter::UnitsHint::Hertz);
				frequency->setRange(20.0, 20000.0);
				frequency->setDefault(1000.0);
				frequency->setNeutralPointOnScale(1000.0);
				
				return pManifest;
		}
		
		// process audio (called every audio callback)
		void processChannel(
						MyEffectGroupState* pState,
						const CSAMPLE* pInput,
						CSAMPLE* pOutput,
						const mixxx::EngineParameters& engineParameters,
						const EffectEnableState enableState,
						const GroupFeatureState& groupFeatures) override {
				
				Q_UNUSED(groupFeatures);  // if not using beat/tempo info
				
				// get parameter values (cached by EffectProcessor)
				const double amount = m_pAmountParameter->value();
				const double frequency = m_pFrequencyParameter->value();
				
				// calculate derived parameters
				const double omega = 2.0 * M_PI * frequency / 
														 pState->m_sampleRate.value();
				
				// sample-by-sample processing
				for (SINT i = 0; 
						 i < engineParameters.samplesPerBuffer(); 
						 i += engineParameters.channelCount()) {
						
						// stereo processing (left + right)
						for (SINT ch = 0; ch < engineParameters.channelCount(); ++ch) {
								CSAMPLE input = pInput[i + ch];
								
								// DSP algorithm here
								CSAMPLE output = input * amount + 
																pState->m_prevSample * (1.0 - amount);
								
								// update state
								pState->m_prevSample = input;
								
								// write output
								pOutput[i + ch] = output;
						}
				}
		}
		
	private:
		// cached parameter pointers (set by EffectProcessor)
		EngineEffectParameter* m_pAmountParameter;
		EngineEffectParameter* m_pFrequencyParameter;
		
		// initialize parameter pointers (called once after manifest creation)
		void loadEngineEffectParameters(
						const QMap<QString, EngineEffectParameter*>& parameters) override {
				m_pAmountParameter = parameters.value("amount");
				m_pFrequencyParameter = parameters.value("frequency");
		}
};

#endif // MYEFFECT_H

register effect (src/effects/backends/builtin/builtinbackend.cpp):

#include "effects/backends/builtin/myeffect.h"

QList<QString> BuiltInBackend::getEffectIds() const {
		QList<QString> effectIds;
		effectIds.append(FilterEffect::getId());
		effectIds.append(ReverbEffect::getId());
		effectIds.append(MyEffect::getId());  // add your effect
		// ...
		return effectIds;
}

EffectManifestPointer BuiltInBackend::getManifest(const QString& effectId) const {
		if (effectId == FilterEffect::getId()) {
				return FilterEffect::getManifest();
		} else if (effectId == ReverbEffect::getId()) {
				return ReverbEffect::getManifest();
		} else if (effectId == MyEffect::getId()) {
				return MyEffect::getManifest();  // register manifest
		}
		// ...
		return EffectManifestPointer();
}

EffectPointer BuiltInBackend::instantiateEffect(const QString& effectId) {
		if (effectId == MyEffect::getId()) {
				return EffectPointer(new MyEffect());  // factory method
		}
		// ...
		return EffectPointer();
}

wet/dry mixing (handled automatically by EffectProcessor):

// effect processor automatically mixes wet/dry based on meta parameter
// your processChannel() receives dry input, writes wet output
// mixer does: finalOutput = dryInput * (1-mix) + wetOutput * mix

// if effect doesn't need ramping from dry:
pManifest->setEffectRampsFromDry(false);

// if effect needs smooth transition from dry (e.g., reverb tail):
pManifest->setEffectRampsFromDry(true);

using beat/tempo information:

void processChannel(..., const GroupFeatureState& groupFeatures) {
		// access beat grid info
		if (groupFeatures.has_beat_length_sec) {
				double beatLength = groupFeatures.beat_length_sec;
				// sync effect to tempo (e.g., delay time = beatLength / 4)
		}
		
		if (groupFeatures.has_beat_fraction) {
				double beatFraction = groupFeatures.beat_fraction;
				// trigger effect on beat (e.g., 0.0 = on beat, 0.5 = halfway)
		}
}

unit test template (src/test/effects/myeffect_test.cpp):

#include <gtest/gtest.h>
#include "effects/backends/builtin/myeffect.h"
#include "test/mixxxtest.h"

class MyEffectTest : public MixxxTest {
	protected:
		void SetUp() override {
				m_pEffect = std::make_unique<MyEffect>();
				m_engineParameters = mixxx::EngineParameters(
						mixxx::audio::SampleRate(44100), 512);
		}
		
		std::unique_ptr<MyEffect> m_pEffect;
		mixxx::EngineParameters m_engineParameters;
};

TEST_F(MyEffectTest, ProcessSilence) {
		// create state
		auto pState = std::make_unique<MyEffectGroupState>(m_engineParameters);
		
		// prepare buffers
		CSAMPLE input[512] = {0};  // silence
		CSAMPLE output[512];
		
		// process
		m_pEffect->processChannel(pState.get(), input, output,
				m_engineParameters, EffectEnableState::Enabled,
				GroupFeatureState());
		
		// verify output is also silence
		for (int i = 0; i < 512; ++i) {
				EXPECT_FLOAT_EQ(output[i], 0.0f);
		}
}

CMakeLists.txt integration:

target_sources(mixxx-lib PRIVATE
		src/effects/backends/builtin/myeffect.h
		src/effects/backends/builtin/myeffect.cpp
)

Effect Parameter Mapping

EffectManifest declares parameters with metadata:

EffectManifestPointer pManifest(new EffectManifest());
pManifest->setId("org.mixxx.effects.reverb");
pManifest->setName("Reverb");

EffectManifestParameterPointer decay = pManifest->addParameter();
decay->setId("decay");
decay->setName("Decay Time");
decay->setRange(0.0, 1.0);
decay->setDefault(0.5);
decay->setControlHint(EffectManifestParameter::ControlHint::KNOB_LINEAR);

EffectParameter connects controls to processor:

  • UI/controller → ControlObject → EffectParameter → EffectProcessor
  • Automatic control generation for each parameter
  • Meta-parameter links multiple parameters for macro control

Effects architecture reference:

History:

  • Pre-1.10 (2001-2011): No effects system - external processing only
  • 1.10 (2012): First effects - simple filter and flanger, inline processing
  • 1.12 (2014): Comprehensive EffectsManager architecture - chains, routing, EffectSlot/EffectProcessor separation
  • 2.0 (2017): LV2 plugin support - backend abstraction for built-in and external effects
  • 2.1 (2018): Per-channel EffectState for memory isolation after thread safety issues discovered
  • 2.2 (2019): QuickEffect system refactored to use effect chain architecture
  • 2.3 (2021): Effect parameter meta-knobs for macro control of multiple parameters

Audio File Formats and Extensions

This section covers Mixxx's advanced audio format support beyond standard MP3/FLAC/WAV files, including stem files (multi-track audio), module files (tracker formats), and the plugin system for effect backends. Understanding these formats is essential for working with specialized audio content.

Why This Matters: Stem files represent the future of DJing—separating drums, bass, vocals, and melody for creative mixing impossible with traditional tracks. The .stem.mp4 format (Native Instruments standard) requires careful handling of multiple interleaved audio streams. Module file support (.mod, .xm, .it) via libopenmpt enables playback of retro tracker music. LV2 plugin loading extends Mixxx's effects without recompilation.

Stems

Mixxx has native support for stem files - multi-track audio files containing separately controllable instrumental parts (drums, bass, vocals, other).

Stem Format:

  • File Format: .stem.mp4 - MP4 container with multiple AAC audio streams
  • Channel Layout: Up to 4 stereo stems (8 channels total) + stereo mix
  • Metadata: Per-stem labels and colors embedded in file
  • Standard: Native Instruments STEMS format

Per-Stem Controls (available for each stem):

[ChannelN],stem_X_gain        // Individual stem volume (X = 1-4)
[ChannelN],stem_X_mute        // Mute individual stem (boolean)
[ChannelN],stem_X_pfl         // Pre-fader listen (headphone cue)
[ChannelN],stem_all_gain      // Master gain for all stems

Stem Processing Architecture:

class SoundSourceSTEM : public SoundSource {
		// Decodes multiple audio streams from single file
		std::vector<std::unique_ptr<SoundSourceSingleSTEM>> m_pStereoStreams;
		
		// Can open in two modes:
		// 1. Stereo mode: Reads pre-mixed stereo (2 channels)
		// 2. Stem mode: Reads all stems separately (8 channels)
};

class EngineDeck {
		// Per-stem gain/mute controls
		std::array<ControlProxy*, kMaxSupportedStems> m_pStemGain;
		std::array<ControlProxy*, kMaxSupportedStems> m_pStemMute;
		
		// Audio processing: Mix stems with individual controls
		void processStemAudio(CSAMPLE* pOutput, const int iBufferSize);
};

UI Integration:

  • Waveform: WaveformRendererStem displays color-coded stem waveforms
  • Widgets: WStemLabel shows stem names/colors, WTrackStemMenu for stem control
  • QML: QmlStemsModel exposes stems to modern QML UI

Use Cases:

  • Live remixing: Isolate drums, bass, vocals during performance
  • Mashups: Combine vocals from one track with instrumentals from another
  • Practice/Learning: Mute specific instruments to play along
  • Creative mixing: Apply different effects to different stems

Limitations:

  • Maximum 4 stems per track (kMaxSupportedStems = 4)
  • Higher CPU usage than stereo playback (decoding 4x streams, Per-Feature Performance Costs)
  • Limited stem file availability (format-specific)

Code location: src/sources/soundsourcestem.h, src/engine/channels/enginedeck.h, src/track/steminfo.h


Module Files

Mixxx supports module files (also known as tracker music) - a family of music file formats originating from music trackers on Amiga and PC platforms.

Module Format Support:

  • File Extensions: .mod, .xm, .s3m, .it, .med, .mtm, .stm, .669, etc.
  • Backend: libmodplug library
  • Build Flag: MODPLUG (optional dependency)
  • Legacy Formats: Preserves retro game and demoscene music

Module Characteristics:

  • Pattern-based sequencing: Notes arranged in patterns/tracks
  • Embedded samples: Audio samples stored within the file
  • Small file sizes: Typically 10-500KB (samples + patterns)
  • No waveform: Cannot display traditional waveforms (pattern-based, not continuous audio)

Module File Structure:

// Module files contain:
// - Sample data (8-bit/16-bit PCM)
// - Pattern data (note + instrument + effect commands)
// - Playback tempo/speed information
// - Channel assignments (typically 4-32 channels)

Playback Integration:

class SoundSourceModPlug : public SoundSource {
		// Decodes module files to PCM audio in real-time
		// Handles tempo changes, pattern loops, effect commands
		// Renders tracker patterns to continuous audio stream
};

DJ Considerations:

  • Beat detection: May not work reliably (irregular tempo changes)
  • Key detection: Often not meaningful (chiptune/retro style)
  • BPM analysis: Module tempo != standard BPM
  • Waveform display: Shows rendered audio, not pattern structure
  • Best use case: Retro gaming events, chiptune sets, demoscene parties

Historical Context: Module files were the dominant music format in:

  • Amiga demoscene (1987-1995): ProTracker, OctaMED
  • PC demoscene (1990s-2000s): FastTracker 2, Impulse Tracker, Scream Tracker
  • Video games: Used for music in DOS/Amiga games due to small size
  • Modern revival: Chiptune artists still create module files

Limitations:

  • No hotcue support (unpredictable sample positions in pattern-based format, CueDAO)
  • Loop detection may fail (pattern-based looping vs. time-based, LoopingControl)
  • Sync features limited (non-standard tempo systems, Master Sync)
  • Best for listening/playback rather than active mixing

Code location: src/sources/soundsourcemodplug.h Build configuration: MODPLUG feature flag detailed in Feature Flags and Build System


Plugin System

Mixxx supports third-party audio plugins through the EffectsBackend system, enabling extensibility without modifying core code.

Supported Plugin Types:

Backend Type Format Location Use Case
Built-in C++ classes src/effects/backends/builtin/ Core effects (EQ, reverb, filters, etc.)
LV2 LV2 plugins System LV2 paths Third-party audio effects
AudioUnit macOS AU System AudioUnit paths macOS-native effects

Plugin Discovery:

class EffectsBackend {
		virtual EffectBackendType getType() const = 0;
		virtual const QList<QString> getEffectIds() const = 0;
		virtual EffectManifestPointer getManifest(const QString& effectId) const = 0;
		virtual std::unique_ptr<EffectProcessor> createProcessor(
						const EffectManifestPointer pManifest) const = 0;
};

Adding New Plugin Types: To support additional plugin formats (e.g., VST, CLAP):

  1. Subclass EffectsBackend for enumeration and instantiation
  2. Subclass EffectProcessorImpl for DSP processing
  3. Create EffectManifest describing parameters
  4. Register backend with EffectsBackendManager

Extension Opportunities:

  • Effects plugins: LV2, AudioUnit (implemented); VST, CLAP (potential)
  • Audio decoders: FFmpeg provides most formats; custom decoders via SoundSource subclass
  • Analysis plugins: Beat/key detection via AnalyzerPlugin interface
  • Controller scripts: JavaScript engine allows custom MIDI/HID mappings without compilation

Audio format architecture reference:

History:

  • 1.0 (2002): libsndfile (WAV/AIFF) and libvorbis (OGG) - foundational audio format support
  • 1.1 (2003): MP3 decoding via MAD library
  • 1.6 (2008): SoundSource abstraction enabling pluggable decoders
  • 1.7 (2009): FLAC support
  • 1.11 (2013): Module file support (.mod, .xm, .it) via libopenmpt - tracker music
  • 2.0 (2017): LV2 effect plugin support - extended beyond decoders to DSP
  • 2.4.0 (2024): Improved FFmpeg integration for M4A/AAC decoding
  • 2.6.0 (main branch): Full free FFmpeg on Windows, revolutionary stem file support (.stem.mp4) - isolate drums/bass/vocals/melody

Track Analysis Pipeline

This section explains Mixxx's automated track analysis system that extracts musical metadata—BPM, beat grids, musical key, loudness normalization, and visual waveforms—from audio files. Analysis runs in background threads to avoid blocking the GUI, with results stored in the database for instant access during performance.

Why This Matters: Track analysis is computationally expensive (minutes per track for large libraries). Understanding the AnalyzerQueue scheduling and the plugin-based analyzer architecture helps optimize analysis speed. The Queen Mary DSP library for BPM detection and libKeyFinder for key detection are external dependencies with their own quirks. Waveform generation affects both disk usage and rendering performance.

Analyzer Plugin System

AnalyzerBeats, AnalyzerKey, AnalyzerWaveform inherit from Analyzer base class:

class Analyzer {
	public:
		virtual bool initialize(const mixxx::AudioSource::Pointer& pAudioSource,
													 int sampleRate,
													 int totalSamples) = 0;
		
		virtual bool process(const CSAMPLE* pIn, const int iLen) = 0;
		
		virtual void cleanup() = 0;
		
		virtual void finalize(TrackPointer pTrack) = 0;
};

AnalyzerQueue (.cpp) coordinates analysis in background thread:

  • Processes tracks from library scanner or manual analysis requests
  • Runs multiple analyzers in pipeline on same audio data
  • Caches results in database for performance
  • Emits progress signals for UI feedback

BPM Detection

AnalyzerBeats detects tempo and generates beat grid:

class AnalyzerBeats : public Analyzer {
		// Uses QM Vamp plugin or SoundTouch for beat detection
		void finalize(TrackPointer pTrack) override {
				pTrack->trySetBeats(m_pBeats);
				pTrack->trySetBpm(m_bpm);
		}
};

Beat grid storage:

  • Beats object stores beat positions and BPM
  • Constant BPM or variable BPM (beat map)
  • Saved to track_locations table (beats_version, beats_sub_version, beats blob)

Manual adjustment:

  • Controls: [ChannelN],beats_translate_earlier/later - shift grid
  • Controls: [ChannelN],beats_adjust_faster/slower - adjust BPM
  • BpmControl manages beat grid and sync calculations

Beat Detection Algorithm

mixxx uses multi-stage beat detection via the QM Vamp plugin (Queen Mary University) or internal algorithms.

stage 1: onset detection (identify percussive hits):

// spectral flux - measure of change in frequency spectrum
double calculateSpectralFlux(const float* spectrum, const float* prevSpectrum, 
															int binCount) {
		double flux = 0.0;
		for (int i = 0; i < binCount; ++i) {
				// sum of positive differences (increase in energy)
				double diff = spectrum[i] - prevSpectrum[i];
				if (diff > 0) {
						flux += diff;
				}
		}
		return flux;
}

// onset detection function (ODF)
void detectOnsets(const CSAMPLE* pSamples, int sampleCount, 
									std::vector<int>& onsetPositions) {
		const int hopSize = 512;  // advance by 512 samples each frame
		const int fftSize = 2048;
		
		std::vector<float> prevSpectrum(fftSize / 2);
		std::vector<float> currentSpectrum(fftSize / 2);
		
		for (int pos = 0; pos < sampleCount - fftSize; pos += hopSize) {
				// compute FFT of current window
				computeFFT(&pSamples[pos], fftSize, currentSpectrum.data());
				
				// calculate spectral flux
				double flux = calculateSpectralFlux(
						currentSpectrum.data(), 
						prevSpectrum.data(), 
						fftSize / 2);
				
				// peak picking: onset if flux exceeds threshold and is local maximum
				if (isLocalMaximum(flux, pos / hopSize) && 
						flux > adaptiveThreshold(pos / hopSize)) {
						onsetPositions.push_back(pos);
				}
				
				prevSpectrum = currentSpectrum;
		}
}

stage 2: tempo estimation (find most likely BPM):

// autocorrelation - find repeating patterns
double estimateTempo(const std::vector<int>& onsets, int sampleRate) {
		const int minBpm = 60;   // 1 beat per second
		const int maxBpm = 200;  // 3.33 beats per second
		
		// convert BPM range to sample lag range
		int minLag = (60.0 * sampleRate) / maxBpm;  // samples between beats @ 200 BPM
		int maxLag = (60.0 * sampleRate) / minBpm;  // samples between beats @ 60 BPM
		
		// autocorrelation: how well does onset pattern match itself shifted?
		std::vector<double> autocorr(maxLag - minLag);
		
		for (int lag = minLag; lag < maxLag; ++lag) {
				double correlation = 0.0;
				int count = 0;
				
				// for each onset, check if there's another onset 'lag' samples later
				for (int i = 0; i < onsets.size(); ++i) {
						for (int j = i + 1; j < onsets.size(); ++j) {
								int diff = onsets[j] - onsets[i];
								if (std::abs(diff - lag) < 100) {  // tolerance window
										correlation += 1.0;
										count++;
								}
						}
				}
				
				autocorr[lag - minLag] = count > 0 ? correlation / count : 0.0;
		}
		
		// find peak in autocorrelation = most likely beat period
		int peakLag = minLag + std::distance(autocorr.begin(), 
																					std::max_element(autocorr.begin(), 
																													autocorr.end()));
		
		// convert lag to BPM
		double bpm = (60.0 * sampleRate) / peakLag;
		
		// check for half-time / double-time confusion
		if (bpm < 90) bpm *= 2;      // likely detected half-time
		if (bpm > 160) bpm /= 2;     // likely detected double-time
		
		return bpm;
}

stage 3: beat tracking (align beats to grid):

// dynamic programming to find optimal beat grid
void trackBeats(const std::vector<int>& onsets, double estimatedBpm, 
								int sampleRate, mixxx::BeatsPointer& pBeats) {
		
		double beatInterval = (60.0 * sampleRate) / estimatedBpm;
		
		// find first beat (look for strong onset near beginning)
		int firstBeat = findFirstStrongOnset(onsets, beatInterval);
		
		// generate beat grid
		std::vector<double> beatPositions;
		double currentBeat = firstBeat;
		
		while (currentBeat < totalSamples) {
				// snap to nearest onset (within tolerance)
				int nearestOnset = findNearestOnset(onsets, currentBeat, beatInterval * 0.2);
				
				if (nearestOnset >= 0) {
						beatPositions.push_back(nearestOnset);
						currentBeat = nearestOnset + beatInterval;
				} else {
						// no nearby onset, stick to grid
						beatPositions.push_back(currentBeat);
						currentBeat += beatInterval;
				}
		}
		
		// create Beats object
		pBeats = mixxx::Beats::fromBeatPositions(beatPositions, sampleRate);
}

qm vamp plugin implementation:

// mixxx uses QM Vamp plugin (preferred method)
#include "vamp-hostsdk/PluginLoader.h"

void analyzeBpmVamp(const QString& filePath, double* bpm, mixxx::BeatsPointer* pBeats) {
		Vamp::Plugin* plugin = Vamp::HostExt::PluginLoader::getInstance()
				->loadPlugin("qm-vamp-plugins:qm-tempotracker", 
										 sampleRate, 
										 Vamp::HostExt::PluginLoader::ADAPT_ALL_SAFE);
		
		if (!plugin) {
				// fallback to internal beat detection
				analyzeBpmInternal(filePath, bpm, pBeats);
				return;
		}
		
		// process audio through plugin
		plugin->initialise(1, 512, 512);  // 1 channel, step 512, block 512
		
		Vamp::Plugin::FeatureSet features = plugin->process(audioData, timestamp);
		
		// extract BPM and beats from plugin output
		for (const auto& feature : features) {
				if (feature.hasTimestamp) {
						double beatTime = feature.timestamp.toDouble();
						beatPositions.push_back(beatTime * sampleRate);
				}
		}
		
		*bpm = calculateAverageBpm(beatPositions);
		*pBeats = mixxx::Beats::fromBeatPositions(beatPositions, sampleRate);
		
		delete plugin;
}

challenges and solutions:

  • tempo ambiguity: 120 BPM vs 60 BPM (half-time) vs 240 BPM (double-time)
    • solution: bias toward 90-160 BPM range, use onset density
  • variable tempo: track speeds up/slows down
    • solution: beat map instead of constant BPM grid
  • complex rhythms: polyrhythms, syncopation, triplets
    • solution: multi-scale analysis, onset strength weighting
  • genre differences: electronic (strong kick) vs acoustic (subtle)
    • solution: adaptive thresholding, frequency band separation

accuracy metrics:

  • tolerance: beat within ±70ms of actual beat = correct
  • typical accuracy: 85-95% on electronic music, 70-85% on acoustic
  • failure cases: ambient (no clear beat), classical (tempo rubato)

Key Detection

AnalyzerKey (.cpp) detects musical key using KeyFinder library:

class AnalyzerKey : public Analyzer {
		// Analyzes chromatic content, outputs key in Lancelot/Camelot notation
		void finalize(TrackPointer pTrack) override {
				pTrack->setKeyText(m_resultKey);
		}
};

Key notation formats:

  • Lancelot: 1A-12A (minor), 1B-12B (major)
  • Camelot: Same as Lancelot
  • Traditional: C major, Am, etc.
  • OpenKey: Same as Lancelot but different numbering

Harmonic mixing: Keys within ±1 number or same number different letter are compatible.

Waveform Generation

AnalyzerWaveform (.cpp) generates visual waveform summary:

class AnalyzerWaveform : public Analyzer {
		// Creates filtered and downsampled waveform for display
		// Stores: Low/mid/high frequency bands, RMS, peak values
		
		void finalize(TrackPointer pTrack) override {
				pTrack->setWaveform(m_waveform);
				pTrack->setWaveformSummary(m_waveformSummary);
		}
};

Waveform data structure:

  • Multi-resolution: Full waveform + summary for zoom levels
  • Per-frequency bands: Low/mid/high separated for colored display
  • Compressed format saved to database for quick loading
  • File location: Track analysis cache (library.db)

Waveform colors:

  • Low frequencies (bass): Red/orange
  • Mid frequencies (vocals): Green/yellow
  • High frequencies (hi-hats): Blue/cyan

ReplayGain Analysis

AnalyzerGain (.cpp) calculates loudness for volume normalization:

class AnalyzerGain : public Analyzer {
		// Implements ReplayGain 2.0 specification
		// Calculates track gain, peak, and loudness (LUFS)
		
		void finalize(TrackPointer pTrack) override {
				pTrack->setReplayGain(ReplayGain(m_gain, m_peak));
		}
};

ReplayGain application:

  • Control: [ChannelN],replaygain - applies calculated gain
  • Prevents clipping while normalizing perceived loudness
  • Stored in track metadata and database

Analysis trigger points:

  1. Auto-analysis: When track added to library (if enabled)
  2. Manual: Right-click track → "Analyze"
  3. Batch: "Analyze" menu → analyze entire library
  4. On-load: First time track is loaded (legacy mode)

Performance: Multi-threaded analysis using QThreadPool, typically 0.5-2x realtime speed per track.

Track analysis architecture reference:

History:

  • 1.6 (2008): Basic BPM detection using SoundTouch - accuracy was poor
  • 1.8 (2010): Queen Mary DSP library integration - dramatically improved BPM detection accuracy, introduced beat grid generation
  • 1.9 (2011): Waveform generation moved from on-demand to pre-analyzed storage - drastically improved track loading performance
  • 1.10 (2012): AnalyzerQueue threading model refactored to use QThreadPool - parallel analysis enabled
  • 1.11 (2013): Key detection via libKeyFinder added - harmonic mixing with Camelot wheel notation
  • 2.0 (2017): ReplayGain analysis using libebur128 - more accurate loudness normalization
  • 2.1 (2018): Analysis pipeline refactored from blocking to fully asynchronous
  • 2.2 (2019): Protocol Buffers adopted for beat grid storage - forward/backward compatibility for beat data
  • 2.3 (2021): Background analysis of entire libraries with multi-core threading
  • 2024: Analysis speed improved 10x from 2008 through algorithmic improvements and parallelization

Waveform Rendering

This section details Mixxx's real-time waveform visualization system, which renders scrolling waveform displays at 60fps while the audio thread plays audio. The system has two distinct components: scrolling waveforms (WWaveformViewer, zoomed detail view) and overview waveforms (WOverview, compressed full-track view). Both support multiple rendering backends (OpenGL, QPainter) and visualization types with pre-analyzed data for performance. Main branch (2.6.0) introduces the Rendergraph library (src/rendergraph/) for improved rendering architecture.

Why This Matters: Waveform rendering runs on the GUI thread at 60fps, reading playback position from atomic controls without blocking the audio thread (Real-Time Audio Thread Requirements). Understanding the renderer hierarchy and caching strategy is essential for adding new waveform visualizations. OpenGL shader-based rendering provides smooth 60fps even on integrated graphics, while the QPainter fallback ensures compatibility with systems lacking GL support. The gain modes (normalize vs ReplayGain) affect how waveform amplitudes are displayed, critical for visual beatmatching (ReplayGain Analysis).

Overview Waveform Architecture

WOverview (.cpp) provides a compressed full-track visualization with markers, showing the entire track in a single horizontal bar. This is the small waveform above/below the main waveform in most skins.

Core functionality:

class WOverview : public WWidget {
		// single-row compressed view of entire track
		// shows: waveform envelope, cue points, beat grid, loop ranges
		// clickable: jump to any position in track
		
	private:
		void paintEvent(QPaintEvent* event) override {
				QPainter painter(this);
				drawWaveformPixmap(&painter);      // pre-rendered waveform
				drawMinuteMarkers(&painter);       // time ruler marks
				drawRangeMarks(&painter, ...);     // loop in/out markers
				drawMarks(&painter, ...);          // cue points, hotcues
				drawMarkLabels(&painter, ...);     // hotcue label text
				drawPlayPosition(&painter);        // current playhead
				drawPlayedOverlay(&painter);       // dim played sections
		}
		
		QImage m_waveformSourceImage;     // full-res waveform data
		QImage m_waveformImageScaled;     // scaled to widget size
		WaveformMarkSet m_marks;          // cue points, hotcues
};

Waveform types (user-selectable in preferences, src/preferences/dialog/dlgprefwaveformdlg.ui): - Empty: no waveform (minimal CPU usage) - Simple: mono waveform envelope (fastest) - RGB: separate low/mid/high frequency bands (most detail) - HSV: hue-coded frequency content - Filtered: single-color waveform with filtering

Gain Modes

Gain modes (affects amplitude scaling, configurable in Preferences → Waveforms):

1. Normalize to peak (default):

// scales waveform so peak amplitude reaches full height
// each track normalized independently
float scaleFactor = 1.0f / m_waveformPeak;
// result: visual consistency, but ignores actual loudness
- **Benefit**: all tracks fill the widget height equally
- **Drawback**: loud and quiet tracks look the same height
- **Use case**: visual consistency across tracks

2. Use "Global" gain and ReplayGain:

// scales by ReplayGain + global waveform gain setting
// reflects actual loudness normalization
float replayGainFactor = calculateReplayGain();
float globalGain = m_pConfig->getValue(
		ConfigKey("[Waveform]", "global_visual_gain"));
float scaleFactor = replayGainFactor * globalGain;
// result: louder tracks appear taller, quieter shorter
- **Benefit**: visual representation matches loudness
- **Drawback**: quiet tracks may be too small
- **Use case**: beatmatching by visual loudness matching

Implementation (src/widget/woverview.cpp):

void WOverview::slotScalingChanged() {
		bool useReplayGain = m_pReplayGainEnabled->toBool();
		if (useReplayGain) {
				// apply ReplayGain + global visual gain
				double replayGain = m_pReplayGain->get();
				m_scaleFactor = pow(10.0, replayGain / 20.0) * globalGain;
		} else {
				// normalize to peak
				m_scaleFactor = 1.0 / m_waveformPeak;
		}
		update();  // trigger repaint with new scale
}

Marker Rendering

Minute markers (visual time ruler, 2.6.0+):

void WOverview::drawMinuteMarkers(QPainter* pPainter) {
		if (!m_pMinuteMarkersControl->toBool()) return;
		
		double trackSeconds = getTrackSamples() / sampleRate / 2.0;
		
		// draw marker every 60 seconds
		for (int minute = 1; minute * 60.0 < trackSeconds; ++minute) {
				double seconds = minute * 60.0;
				int xPos = valueToPosition(seconds / trackSeconds);
				pPainter->drawLine(xPos, 0, xPos, height());
		}
}

Hotcue label rendering (src/waveform/waveformmarklabel.cpp):

class WaveformMarkLabel {
		void prerender(QPointF bottomLeft, const QPixmap& icon,
									 QString text, const QFont& font) {
				QPainter painter(&m_pixmap);
				painter.drawRoundedRect(rect, 2.0, 2.0);  // background
				if (!icon.isNull()) painter.drawPixmap(pos, icon);
				// elide text to fit widget width
				QString elidedText = fontMetrics.elidedText(
						text, Qt::ElideRight, availableWidth);
				painter.drawText(textPos, elidedText);
		}
};

Label overlap prevention:

void WOverview::drawMarkLabels(QPainter* pPainter) {
		QRectF lastLabelRect;
		for (const auto& pMark : m_marks.getSortedMarks()) {
				QRectF currentRect = calculateLabelRect(pMark);
				if (currentRect.intersects(lastLabelRect)) {
						continue;  // hide overlapping label
				}
				m_cuePositionLabel.draw(pPainter);
				lastLabelRect = currentRect;
		}
}

Scrolling Waveform Architecture

WWaveformViewer provides a zoomed detail view showing 10-60 seconds of the track with high temporal resolution.

Renderer Hierarchy

WaveformRendererAbstract (.cpp) base class for all renderers:

class WaveformRendererAbstract {
	public:
		virtual bool init() = 0;
		virtual void draw(QPainter* painter, QPaintEvent* event) = 0;
		virtual void resize(int width, int height);
	protected:
		WaveformWidgetAbstract* m_waveformWidget;
};

Renderer types (src/waveform/renderers/): - WaveformRendererRGB (.cpp) - classic RGB waveform - Bands: red (bass <250Hz), green (mids 250Hz-4kHz), blue (highs >4kHz) - Use case: identify kick drums (red), vocals (green), cymbals (blue) - WaveformRendererFiltered (.cpp) - filtered single-color waveform - Simple: mono waveform with basic filtering, lower CPU than RGB - WaveformRendererHSV (.cpp) - HSV color-coded waveform - Hue: dominant frequency, Saturation: frequency purity, Value: amplitude - WaveformRendererSimple (.cpp) - low-CPU simple waveform - Oscilloscope-style: just amplitude, no frequency analysis - WaveformRendererStem (.cpp) - multi-stem rendering (2.6.0+) - 4 colors: one per stem (drums/bass/vocals/melody), stacked vertically

Renderer stack (layers composed back to front):

WaveformWidget rendering order:
	1. WaveformRendererBackground     // background color/image
	2. WaveformRendererPreroll        // dim area before track start
	3. WaveformRendererSignalBase     // main waveform (RGB/Filtered/etc)
	4. WaveformRendererBeat           // beat grid tick marks
	5. WaveformRendererMark           // cue points, hotcues
	6. WaveformRendererMarkRange      // loop in/out regions
	7. WaveformRendererEndOfTrack     // red line at track end
	8. WaveformRendererSlipMode       // slip mode indicator (2.6.0+)

Example: RGB waveform renderer (src/waveform/renderers/waveformrendererrgb.cpp):

void WaveformRendererRGB::draw(QPainter* painter, QPaintEvent* event) {
		ConstWaveformPointer pWaveform = m_waveformWidget->getWaveform();
		
		// get visible range
		double firstVisualIndex, lastVisualIndex;
		m_waveformWidget->getVisibleRange(&firstVisualIndex, &lastVisualIndex);
		
		// draw RGB frequency bands
		for (int x = 0; x < m_waveformWidget->getWidth(); ++x) {
				double sampleIndex = firstVisualIndex + 
						(x / width) * (lastVisualIndex - firstVisualIndex);
				
				// get pre-analyzed RGB values
				WaveformData data = pWaveform->getScaledData(sampleIndex);
				
				// draw three colored bars
				painter->setPen(QColor(data.low, 0, 0));   // red=bass
				painter->drawLine(x, centerY, x, centerY + data.low);
				
				painter->setPen(QColor(0, data.mid, 0));   // green=mids
				painter->drawLine(x, centerY, x, centerY + data.mid);
				
				painter->setPen(QColor(0, 0, data.high));  // blue=highs
				painter->drawLine(x, centerY, x, centerY + data.high);
		}
}

OpenGL vs Software Rendering

OpenGL rendering (GPU-accelerated, src/waveform/widgets/allshader/):

class AllShaderWaveformWidget : public WaveformWidgetAbstract, 
																 public QOpenGLWidget {
		// modern OpenGL 3.x+ shader-based rendering (2.6.0+)
		// uses vertex buffers and shaders for 60fps
		void paintGL() override {
				// upload waveform data to GPU as vertex buffer
				glBindBuffer(GL_ARRAY_BUFFER, m_waveformVBO);
				// render with custom GLSL shaders
				m_shaderProgram.bind();
				glDrawArrays(GL_TRIANGLE_STRIP, 0, vertexCount);
		}
};

Software rendering (CPU fallback, src/waveform/widgets/qtwaveformwidget.cpp):

class QtWaveformWidget : public WaveformWidgetAbstract, public QWidget {
		// uses QPainter for software rendering
		// fallback when OpenGL unavailable or disabled
		void paintEvent(QPaintEvent* event) override {
				QPainter painter(this);
				for (auto* renderer : m_rendererStack) {
						renderer->draw(&painter, event);
				}
		}
};

Rendering backend selection (src/waveform/waveformwidgetfactory.cpp): 1. check user preference: [Waveform],waveform_type in mixxx.cfg 2. try OpenGL if available and [Waveform],use_opengl=1 3. fall back to Qt software rendering if OpenGL fails or disabled 4. log selected backend: "Waveform: Using AllShaderWaveformWidget (OpenGL)"

Performance comparison (Performance Characteristics): - OpenGL (AllShader): 60fps, ~5-10% CPU per waveform, ~50-80MB VRAM per waveform - OpenGL (Legacy): 60fps, ~8-15% CPU per waveform (deprecated, 2.6.0+) - Software (QPainter): 30fps, ~15-25% CPU per waveform, ~20-40MB RAM per waveform - Impact: typical skins show 2-4 waveforms (2 decks × scrolling + overview)

Waveform Caching and Rendering Pipeline

VisualPlayPosition (.cpp) tracks playback position for waveform scrolling:

class VisualPlayPosition : public QObject {
		// smooths position updates for fluid waveform scrolling
		// interpolates between audio buffer callbacks (~200 Hz)
		// to achieve smooth 60fps visual updates
		
		double getEnginePlayPos() const {
				// returns interpolated position between callbacks
				return m_dPosition + m_dPositionIncrement * elapsedMs;
		}
		
		void setPosition(mixxx::audio::FramePos position) {
				// called from audio thread via control update
				m_dPosition = position.value();
				m_lastUpdateTime = QTime::currentTime();
		}
		
	private:
		double m_dPosition;            // last known position from engine
		double m_dPositionIncrement;   // rate of change (for interpolation)
		QTime m_lastUpdateTime;        // for smooth interpolation
};

Texture caching (OpenGL path): - GPU textures: pre-rendered waveform sections cached in VRAM - Vertex buffers: waveform geometry uploaded once, reused across frames - Shader compilation: GLSL shaders compiled once at widget creation - Cache invalidation: track change, zoom level change, skin reload triggers rebuild - Memory: ~50-80MB VRAM per visible waveform

Pixmap caching (software path): - QPixmap cache: pre-rendered sections stored in system RAM - Dirty regions: only repaint changed portions (playhead, markers) - Cache size: ~20-40MB RAM per waveform - Fallback: full repaint if cache invalidated

Zoom levels (user-controlled):

// control: [ChannelN],waveform_zoom
// range: 1 (zoomed out) to 10 (zoomed in)
// effect: changes visible time window

double zoomFactor = m_pZoomControl->get();
double visibleSeconds = 60.0 / zoomFactor;  // 60 sec at zoom=1

// examples:
// zoom=1:  60 seconds visible (wide view, beatmatching)
// zoom=3:  20 seconds visible (default)
// zoom=6:  10 seconds visible (detailed, scratching)

Waveform rendering reference:

History (Waveform Generation, Performance Characteristics, CHANGELOG.md):

  • Pre-1.0 (2001-2006): no waveform display, only position sliders
  • 0.6 (2003): first waveform display—simple amplitude graph, QPainter, ~30% CPU per waveform
  • 0.8-1.6 (2006-2009): basic improvements, still real-time rendering (slow)
  • 1.7 (2009): overview waveform widget (WOverview) introduced for full-track view
  • 1.8 (2010): beat grid overlay on waveforms, track position markers
  • 1.9 (2011): pre-analyzed waveform summaries—eliminated on-the-fly rendering, CPU dropped to ~15%
  • 1.10 (2012): OpenGL acceleration introduced, factory pattern for renderers, ~5-8% CPU per waveform
  • 1.11 (2013-2014): HSV waveform (frequency-coded hue), RGB waveform (default), zoom controls
  • 1.12 (2014): waveform zoom controls (10-60 second views), beat marker improvements
  • 2.0 (2015-2017): major rendering refactor, GPU utilization improvements, colored beat markers
  • 2.1 (2018): improved scrolling performance, VisualPlayPosition interpolation
  • 2.2-2.3 (2019-2021): hotcue label rendering on markers, cue point visualization improvements
  • 2.4 (2024): OpenGL modernization, shader improvements, high-DPI support, AllShaderWaveformWidget
  • 2.5 (2024-12): performance optimizations, memory usage improvements, caching refinements
  • 2.6.0 (main branch): Rendergraph library, minute markers, ReplayGain gain mode, slip mode visualization
    • Rendergraph library (src/rendergraph/): scene graph rendering framework
    • Minute markers: time ruler marks every 60 seconds on overview waveform
    • ReplayGain gain mode: waveform amplitude scaled by loudness (vs peak normalization)
    • Slip mode indicator: visual feedback for slip mode on scrolling waveforms
    • OpenGL ES support: compatibility with embedded platforms (textured waveforms disabled on GLES)
    • Label overlap prevention: hotcue labels hide when overlapping adjacent markers

Library and Database Architecture

This section provides a deep dive into Mixxx's database layer, explaining the DAO (Data Access Object) pattern that isolates SQL from business logic, the TrackPointer pattern for thread-safe shared ownership, and the GlobalTrackCache singleton that ensures one-track-per-file semantics. Understanding these patterns is essential for modifying database schema or adding new library features.

Why This Matters: The Library uses SQLite with complex queries that must remain performant even with 100,000+ track libraries. The DAO pattern makes schema changes safer by isolating SQL. Never use raw Track* pointers—always use TrackPointer (QSharedPointer) or risk dangling pointers when tracks are unloaded. The GlobalTrackCache prevents data inconsistency by ensuring a single Track object per file path.

DAO Pattern (Data Access Objects)

Database access is encapsulated in DAO classes:

class DAO {
	protected:
		QSqlDatabase m_database;
	public:
		virtual void initialize(const QSqlDatabase& database);
};

class TrackDAO : public QObject, public virtual DAO {
		// all SQL operations for the library and track_locations tables
		TrackPointer getTrackByRef(const TrackRef& trackRef) const;
		bool saveTrack(Track* pTrack) const;
		bool addTracksAddDirectory(const QString& dir) const;
};

class CueDAO : public DAO {
		// all SQL operations for the cues table
		bool saveCue(Cue* pCue, TrackId trackId);
		QList<CuePointer> getCuesForTrack(TrackId trackId) const;
};

pattern: each DAO class encapsulates all SQL queries for specific tables, isolating SQL from business logic.

TrackDAO::saveTrack() implementation (src/library/dao/trackdao.cpp):

bool TrackDAO::saveTrack(Track* pTrack) const {
		TrackId trackId = pTrack->getId();
		
		// prepare SQL update with all track metadata
		QSqlQuery query(m_database);
		query.prepare(
				"UPDATE library SET "
				"artist = :artist, "
				"title = :title, "
				"album = :album, "
				"year = :year, "
				"genre = :genre, "
				"tracknumber = :tracknumber, "
				"composer = :composer, "
				"grouping = :grouping, "
				"comment = :comment, "
				"url = :url, "
				"duration = :duration, "
				"bitrate = :bitrate, "
				"samplerate = :samplerate, "
				"channels = :channels, "
				"bpm = :bpm, "
				"replaygain = :replaygain, "
				"key = :key, "
				"key_id = :key_id, "
				"rating = :rating, "
				"timesplayed = :timesplayed, "
				"played = :played, "
				"color = :color, "
				"coverart_source = :coverart_source, "
				"coverart_type = :coverart_type, "
				"coverart_location = :coverart_location, "
				"coverart_hash = :coverart_hash "
				"WHERE id = :id");
		
		// bind all track properties
		query.bindValue(":id", trackId.toVariant());
		query.bindValue(":artist", pTrack->getArtist());
		query.bindValue(":title", pTrack->getTitle());
		query.bindValue(":album", pTrack->getAlbum());
		query.bindValue(":year", pTrack->getYear());
		query.bindValue(":genre", pTrack->getGenre());
		query.bindValue(":tracknumber", pTrack->getTrackNumber());
		query.bindValue(":composer", pTrack->getComposer());
		query.bindValue(":grouping", pTrack->getGrouping());
		query.bindValue(":comment", pTrack->getComment());
		query.bindValue(":url", pTrack->getLocation());
		query.bindValue(":duration", pTrack->getDuration());
		query.bindValue(":bitrate", pTrack->getBitrate());
		query.bindValue(":samplerate", pTrack->getSampleRate().value());
		query.bindValue(":channels", pTrack->getChannels());
		query.bindValue(":bpm", pTrack->getBpm().value());
		query.bindValue(":replaygain", pTrack->getReplayGain().getRatio());
		query.bindValue(":key", pTrack->getKeyText());
		query.bindValue(":key_id", pTrack->getKey());
		query.bindValue(":rating", pTrack->getRating());
		query.bindValue(":timesplayed", pTrack->getTimesPlayed());
		query.bindValue(":played", pTrack->getPlayCounter().isPlayed());
		query.bindValue(":color", pTrack->getColor().value());
		
		// execute update
		if (!query.exec()) {
				LOG_FAILED_QUERY(query) << "failed to save track" << trackId;
				return false;
		}
		
		// save related data via other DAOs
		m_cueDao.saveTrackCues(trackId, pTrack);
		m_analysisDao.saveTrackAnalyses(trackId, pTrack);
		
		return true;
}

CueDAO::getCuesForTrack() implementation (src/library/dao/cuedao.cpp):

QList<CuePointer> CueDAO::getCuesForTrack(TrackId trackId) const {
		QList<CuePointer> cues;
		
		QSqlQuery query(m_database);
		query.prepare(
				"SELECT id, track_id, type, position, length, "
				"       hotcue, label, color "
				"FROM cues "
				"WHERE track_id = :track_id "
				"ORDER BY position");
		query.bindValue(":track_id", trackId.toVariant());
		
		if (!query.exec()) {
				LOG_FAILED_QUERY(query) << "failed to load cues for track" << trackId;
				return cues;
		}
		
		while (query.next()) {
				CuePointer pCue = CuePointer(new Cue(trackId));
				pCue->setId(query.value(0).toInt());
				pCue->setType(static_cast<mixxx::CueType>(query.value(2).toInt()));
				pCue->setPosition(query.value(3).toDouble());
				pCue->setLength(query.value(4).toDouble());
				pCue->setHotCue(query.value(5).toInt());
				pCue->setLabel(query.value(6).toString());
				pCue->setColor(mixxx::RgbColor(query.value(7).toUInt()));
				cues.append(pCue);
		}
		
		return cues;
}

benefits of DAO pattern: - SQL isolation: all queries in one place, easy to review/optimize - type safety: Qt's bindValue() prevents SQL injection - transaction support: wrap multiple DAO calls in single transaction - testing: mock DAOs for unit tests without real database - migration: schema changes localized to DAO layer

Track Pointer Pattern

Tracks are always accessed via reference-counted pointers:

typedef QSharedPointer<Track> TrackPointer;

TrackPointer pTrack = library->getTrackByRef(trackRef);
// Automatically cleaned up when last reference is released

Global Track Cache

A singleton cache ensures only one Track object exists per file:

  • Prevents concurrent modifications to the same track
  • Enforces consistency across the application
  • Reduces memory usage

GlobalTrackCache implementation (src/library/trackcollection.cpp):

class GlobalTrackCache {
	private:
		// hash map: file path → weak pointer to Track
		QHash<QString, QWeakPointer<Track>> m_tracksByPath;
		QMutex m_mutex;  // protects hash map
		
	public:
		TrackPointer lookupTrackByRef(const TrackRef& trackRef) {
				QMutexLocker lock(&m_mutex);
				
				// check cache first
				QWeakPointer<Track> weakPtr = m_tracksByPath.value(trackRef.getLocation());
				TrackPointer pTrack = weakPtr.toStrongRef();
				
				if (pTrack) {
						return pTrack;  // cache hit
				}
				
				// cache miss: load from database
				pTrack = m_trackDAO.loadTrack(trackRef);
				if (pTrack) {
						// store weak pointer (doesn't prevent deletion)
						m_tracksByPath.insert(trackRef.getLocation(), pTrack.toWeakRef());
				}
				
				return pTrack;
		}
		
		// called periodically to remove expired weak pointers
		void purgeExpiredTracks() {
				QMutexLocker lock(&m_mutex);
				
				auto it = m_tracksByPath.begin();
				while (it != m_tracksByPath.end()) {
						if (it.value().isNull()) {
								it = m_tracksByPath.erase(it);  // track deleted, remove entry
						} else {
								++it;
						}
				}
		}
};

weak pointer benefits: - cache doesn't keep tracks alive (memory efficient) - tracks deleted when last strong reference released - cache auto-purges via isNull() checks

Database Schema

library table (main track metadata, src/library/schema.sql):

CREATE TABLE library (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		artist TEXT,
		title TEXT,
		album TEXT,
		album_artist TEXT,
		year TEXT,
		genre TEXT,
		composer TEXT,
		grouping TEXT,
		tracknumber TEXT,
		tracktotal TEXT,
		filetype TEXT,
		comment TEXT,
		url TEXT,
		duration REAL,
		bitrate INTEGER,
		samplerate INTEGER,
		channels INTEGER,
		bpm REAL,
		replaygain REAL,
		replaygain_peak REAL,
		key TEXT,
		key_id INTEGER,
		rating INTEGER,
		timesplayed INTEGER,
		played BOOLEAN DEFAULT 0,
		datetime_added TEXT,
		datetime_modified TEXT,
		color INTEGER,
		coverart_source INTEGER,
		coverart_type INTEGER,
		coverart_location TEXT,
		coverart_hash INTEGER,
		headerParsed INTEGER DEFAULT 0,
		mixxxdeleted INTEGER DEFAULT 0
);

indexes for performance:

-- artist/album/title search (most common queries)
CREATE INDEX idx_library_artist ON library (artist);
CREATE INDEX idx_library_album ON library (album);
CREATE INDEX idx_library_title ON library (title);

-- BPM/key filtering for harmonic mixing
CREATE INDEX idx_library_bpm ON library (bpm);
CREATE INDEX idx_library_key_id ON library (key_id);

-- recently added/played
CREATE INDEX idx_library_datetime_added ON library (datetime_added);
CREATE INDEX idx_library_played ON library (played);

-- file path lookup (cache miss)
CREATE INDEX idx_library_url ON library (url);

cues table (hotcues, main cue, intro/outro markers):

CREATE TABLE cues (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		track_id INTEGER REFERENCES library(id) ON DELETE CASCADE,
		type INTEGER,           -- 0=main, 1=hotcue, 2=intro, 3=outro, etc
		position INTEGER,       -- sample position
		length INTEGER,         -- length in samples (for loops)
		hotcue INTEGER,         -- hotcue number (1-36)
		color INTEGER           -- ARGB color value
);

CREATE INDEX idx_cues_track_id ON cues (track_id);

track_locations table (file path tracking):

CREATE TABLE track_locations (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		location TEXT UNIQUE,     -- file:///path/to/track.mp3
		directory TEXT,           -- /path/to
		filename TEXT,            -- track.mp3
		filesize INTEGER,
		fs_deleted INTEGER DEFAULT 0,
		needs_verification INTEGER DEFAULT 0
);

CREATE INDEX idx_track_locations_location ON track_locations (location);
CREATE INDEX idx_track_locations_directory ON track_locations (directory);

foreign key relationships:

-- library.id ← cues.track_id (one-to-many)
-- library.id ← playlist_tracks.track_id (many-to-many via junction)
-- library.id ← crate_tracks.track_id (many-to-many via junction)

-- enforce referential integrity
PRAGMA foreign_keys = ON;

triggers for automatic updates:

-- update datetime_modified when track metadata changes
CREATE TRIGGER update_library_timestamp 
		AFTER UPDATE ON library
		FOR EACH ROW
		WHEN OLD.artist != NEW.artist 
			OR OLD.title != NEW.title
			OR OLD.album != NEW.album
BEGIN
		UPDATE library 
		SET datetime_modified = datetime('now')
		WHERE id = NEW.id;
END;

Database Migration Guide

schema versioning ensures database compatibility across mixxx versions.

migration architecture:

// src/library/schemamigration.cpp
class SchemaMigration {
	public:
		// current schema version
		static constexpr int kCurrentSchemaVersion = 42;
		
		bool migrateToSchemaVersion(int targetVersion) {
				int currentVersion = getCurrentSchemaVersion();
				
				while (currentVersion < targetVersion) {
						if (!migrateToNextVersion(currentVersion)) {
								return false;  // migration failed
						}
						currentVersion++;
				}
				
				return true;
		}
		
	private:
		bool migrateToNextVersion(int fromVersion);
};

version tracking:

-- settings table stores schema version
CREATE TABLE IF NOT EXISTS settings (
		name TEXT PRIMARY KEY,
		value TEXT
);

INSERT INTO settings (name, value) VALUES ('schema_version', '42');

-- check version before migration
SELECT value FROM settings WHERE name = 'schema_version';

migration example: add color column to library table:

step 1: create migration function (src/library/schemamigration.cpp):

bool migrate41To42(QSqlDatabase& database) {
		qDebug() << "Migrating database schema from version 41 to 42";
		
		// begin transaction for atomic migration
		QSqlQuery query(database);
		if (!database.transaction()) {
				qWarning() << "Failed to start transaction";
				return false;
		}
		
		// add color column
		if (!query.exec("ALTER TABLE library ADD COLUMN color INTEGER DEFAULT 0")) {
				qWarning() << "Failed to add color column:" << query.lastError();
				database.rollback();
				return false;
		}
		
		// create index if needed
		if (!query.exec("CREATE INDEX IF NOT EXISTS idx_library_color ON library (color)")) {
				qWarning() << "Failed to create color index:" << query.lastError();
				database.rollback();
				return false;
		}
		
		// update schema version
		query.prepare("UPDATE settings SET value = :version WHERE name = 'schema_version'");
		query.bindValue(":version", 42);
		if (!query.exec()) {
				qWarning() << "Failed to update schema version:" << query.lastError();
				database.rollback();
				return false;
		}
		
		// commit transaction
		if (!database.commit()) {
				qWarning() << "Failed to commit migration";
				database.rollback();
				return false;
		}
		
		qDebug() << "Successfully migrated to schema version 42";
		return true;
}

step 2: register migration (src/library/schemamigration.cpp):

bool SchemaMigration::migrateToNextVersion(int fromVersion) {
		switch (fromVersion) {
				case 41:
						return migrate41To42(m_database);
				case 42:
						return migrate42To43(m_database);
				// ... other versions
				default:
						qWarning() << "Unknown schema version:" << fromVersion;
						return false;
		}
}

step 3: update schema version constant:

// src/library/schemamigration.h
static constexpr int kCurrentSchemaVersion = 42;  // was 41

backwards compatibility:

// handle old databases gracefully
int schemaVersion = getCurrentSchemaVersion();

if (schemaVersion > kCurrentSchemaVersion) {
		// database from newer Mixxx version
		qWarning() << "Database schema" << schemaVersion 
							 << "is newer than supported" << kCurrentSchemaVersion;
		
		// options:
		// 1. refuse to open (safest)
		// 2. open read-only
		// 3. create backup and try anyway
		
		return false;  // refuse to open
}

if (schemaVersion < kMinimumSupportedSchemaVersion) {
		// database too old, migration path broken
		qWarning() << "Database schema" << schemaVersion 
							 << "is too old, minimum supported is" 
							 << kMinimumSupportedSchemaVersion;
		return false;
}

common migration patterns:

rename column (SQLite limitation workaround):

-- SQLite doesn't support ALTER TABLE RENAME COLUMN before 3.25.0
-- workaround: create new table, copy data, drop old table

-- 1. create new table with correct schema
CREATE TABLE library_new (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		artist TEXT,
		track_title TEXT,  -- renamed from 'title'
		album TEXT
		-- ... other columns
);

-- 2. copy data from old table
INSERT INTO library_new (id, artist, track_title, album)
SELECT id, artist, title, album FROM library;

-- 3. drop old table
DROP TABLE library;

-- 4. rename new table
ALTER TABLE library_new RENAME TO library;

-- 5. recreate indexes
CREATE INDEX idx_library_artist ON library (artist);
CREATE INDEX idx_library_track_title ON library (track_title);

add foreign key constraint (requires recreation):

-- SQLite doesn't support ADD CONSTRAINT
-- must recreate table with foreign key

PRAGMA foreign_keys = OFF;  -- disable temporarily

CREATE TABLE cues_new (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		track_id INTEGER REFERENCES library(id) ON DELETE CASCADE,  -- new FK
		type INTEGER,
		position INTEGER,
		label TEXT,
		color INTEGER
);

INSERT INTO cues_new SELECT * FROM cues;

DROP TABLE cues;

ALTER TABLE cues_new RENAME TO cues;

PRAGMA foreign_keys = ON;  -- re-enable

data transformation (migrate enum values):

-- old: type stored as string ('hotcue', 'main', 'intro')
-- new: type stored as integer (0=main, 1=hotcue, 2=intro)

UPDATE cues SET type = CASE
		WHEN type = 'main' THEN 0
		WHEN type = 'hotcue' THEN 1
		WHEN type = 'intro' THEN 2
		WHEN type = 'outro' THEN 3
		ELSE 0
END;

-- change column type
-- (requires table recreation in SQLite)

migration testing:

// unit test for migration
TEST_F(SchemaMigrationTest, Migrate41To42) {
		// create database with schema version 41
		createSchemaVersion41();
		
		// verify initial state
		EXPECT_EQ(41, getCurrentSchemaVersion());
		EXPECT_FALSE(columnExists("library", "color"));
		
		// run migration
		SchemaMigration migration(m_database);
		EXPECT_TRUE(migration.migrateToSchemaVersion(42));
		
		// verify final state
		EXPECT_EQ(42, getCurrentSchemaVersion());
		EXPECT_TRUE(columnExists("library", "color"));
		
		// verify data preserved
		int trackCount = getTrackCount();
		EXPECT_GT(trackCount, 0);
		
		// verify new column has default value
		QSqlQuery query("SELECT color FROM library LIMIT 1");
		EXPECT_TRUE(query.next());
		EXPECT_EQ(0, query.value(0).toInt());
}

best practices:

  • atomic migrations: use transactions, rollback on error
  • backwards compatible: don't break old data
  • test migrations: unit test with real database files from each version
  • backup database: copy ~/.mixxx/mixxx.db before migration
  • incremental versions: migrate one version at a time, no skipping
  • log everything: detailed logging for debugging failed migrations
  • defensive coding: check query errors, validate state
  • data preservation: never delete data in migration (mark as deprecated instead)

migration sequence on startup:

// src/mixxx.cpp
bool Mixxx::initializeDatabase() {
		// 1. open database
		m_database = QSqlDatabase::addDatabase("QSQLITE");
		m_database.setDatabaseName(dbPath);
		
		if (!m_database.open()) {
				qCritical() << "Failed to open database:" << m_database.lastError();
				return false;
		}
		
		// 2. check schema version
		SchemaMigration migration(m_database);
		int currentVersion = migration.getCurrentSchemaVersion();
		
		if (currentVersion == 0) {
				// new database, create schema
				if (!migration.createSchema()) {
						return false;
				}
		} else if (currentVersion < SchemaMigration::kCurrentSchemaVersion) {
				// old database, migrate
				qDebug() << "Migrating database from version" << currentVersion
								 << "to" << SchemaMigration::kCurrentSchemaVersion;
				
				// backup database before migration
				QFile::copy(dbPath, dbPath + ".backup");
				
				if (!migration.migrateToSchemaVersion(
								SchemaMigration::kCurrentSchemaVersion)) {
						qCritical() << "Database migration failed";
						// restore backup
						QFile::remove(dbPath);
						QFile::rename(dbPath + ".backup", dbPath);
						return false;
				}
				
				// migration successful, remove backup
				QFile::remove(dbPath + ".backup");
		}
		
		return true;
}

handling migration failures:

  • user sees error dialog: "Database migration failed. Backup restored."
  • mixxx refuses to start with corrupted database
  • user options:
    1. restore from automatic backup
    2. rescan library (loses playlists/crates/cues)
    3. report bug with database file

TrackCollection and Library

TrackCollection is the high-level library API:

class TrackCollection {
	public:
		TrackPointer getTrackByRef(const TrackRef& trackRef);
		void saveTrack(Track* pTrack);
		
	private:
		TrackDAO m_trackDao;
		PlaylistDAO m_playlistDao;
		CrateDAO m_crateDao;
		AnalysisDao m_analysisDao;
		GlobalTrackCacheLocker m_trackCache;
};

Library (extends QObject) provides Qt integration:

  • Manages library scanner thread
  • Emits signals for UI updates (trackAdded, trackRemoved)
  • Coordinates track analysis queue
  • Handles external storage detection (USB drives)

LibraryControl exposes library functions as controls:

[Library],sort_column
[Library],sort_order
[Library],font_size_increment
[Library],GoToItem  // Jump to track in library view

Crates and Playlists

Crates - static track collections:

class CrateDAO : public DAO {
		CrateId createCrate(const QString& name);
		bool addTrackToCrate(CrateId crateId, TrackId trackId);
		QList<TrackId> getTrackIds(CrateId crateId);
};

Playlists - ordered track sequences:

class PlaylistDAO : public DAO {
		int createPlaylist(const QString& name);
		bool appendTrackToPlaylist(int playlistId, TrackId trackId);
		void setPlaylistTrackPosition(int playlistId, int position, TrackId trackId);
};

Key differences:

  • Crates: Unordered, manual organization, persistent
  • Playlists: Ordered, can be auto-generated (Auto DJ), support drag-drop reordering
  • Smart Playlists: SQL-based dynamic filtering (not yet implemented)

UI models (QAbstractItemModel subclasses):

  • CrateTableModel - displays crate contents
  • PlaylistTableModel - displays playlist with ordering
  • LibraryTableModel - main library view with sorting/filtering

Track Scanner

DirectoryDAO tracks folders being monitored:

class DirectoryDAO : public DAO {
		void addDirectory(const QString& dir);
		QStringList getDirs();
};

RecursiveScanDirectoryTask scans for new/modified tracks:

  • Runs in background LibraryScanner thread
  • Detects new files via filesystem timestamps
  • Reads metadata (artist, title, BPM, etc.) using TagLib
  • Adds tracks to database via TrackDAO
  • Triggers analysis if auto-analysis enabled

Scanner optimizations:

  • Caches directory modification times to skip unchanged folders
  • Uses file hashes to detect moved files
  • Parallelizes metadata reading across multiple files
  • Respects hidden files and ignores patterns

Relocating tracks:

  • If file moved, scanner can detect by matching hash/metadata
  • Manual relocation via "Relocate" dialog
  • Broken tracks shown with missing icon in library

Library architecture reference:

  • DAO pattern: Data Access Object implementation details in DAO Pattern with SQL isolation
  • Track analysis: How analyzed metadata is stored covered in Track Analysis Pipeline
  • Track pointers: Thread-safe shared ownership explained in Track Pointer Pattern
  • Library subsystem: High-level overview in Library with scanner and cache details
  • Code location: src/library/ contains DAO implementations and table models

History:

  • 0.5 (2002): Simple file list - no database
  • 1.6 (2008): Full SQLite database with basic metadata
  • 1.7 (2009): DAO pattern introduced - isolated SQL from business logic for safer migrations
  • 1.8 (2010): Playlists and crates moved from XML files to database
  • 1.9 (2011): TrackPointer (QSharedPointer<Track>) pattern - eliminated dangling pointer crashes
  • 1.10 (2012): GlobalTrackCache singleton enforces one-Track-per-file after data corruption discovered
  • 1.11 (2013): Database schema versioning and automatic migrations
  • 2.0 (2017): Library scanner rewritten iterative (not recursive) for 100,000+ track libraries
  • 2.1 (2018): BaseTrackCache/BaseTrackTableModel for efficient table views
  • 2.2 (2019): Cover art caching moved from filesystem to database BLOBs

Widget and UI Patterns

This section explains the widget architecture underlying Mixxx's UI system, showing how the mixin pattern enables automatic control binding and how the XML skin system creates declarative layouts without C++ code. Understanding these patterns is essential for adding new widget types or creating custom skins.

Why This Matters: The WBaseWidget mixin pattern is what enables automatic bidirectional binding—change a control from a MIDI controller, and UI widgets auto-update via signals. Change a slider in the UI, and the control updates atomically for the audio thread. The declarative skin XML means UI designers can create entire interfaces without touching C++ code. Understanding ControlParameterWidgetConnection lifecycle is critical to prevent memory leaks.

Widget Hierarchy

Mixxx widgets use a mixin pattern:

class WBaseWidget {
		// Owns ControlParameterWidgetConnection objects
		// Provides bidirectional widget ↔ control binding
	protected:
		virtual void onConnectedControlChanged(double parameter, double value);
};

// Concrete widgets inherit from both QWidget and WBaseWidget
class WPushButton : public QPushButton, public WBaseWidget {
		// Implementation
};

Skin System

Mixxx uses an XML-based declarative UI system:

<PushButton>
	<Group>[Channel1]</Group>
	<Key>play</Key>
	<Connection>
		<ConfigKey>[Channel1],play</ConfigKey>
	</Connection>
</PushButton>

The LegacySkinParser parses the XML, creates widgets, and connects them to controls automatically.

Skin XML Specification

complete widget reference for skin developers.

common attributes (all widgets):

<WidgetName>
	<ObjectName>uniqueId</ObjectName>        <!-- CSS selector -->
	<Tooltip>Hover text</Tooltip>           <!-- tooltip on hover -->
	<TooltipId>play_tooltip</TooltipId>     <!-- shared tooltip ID -->
	<Style>QWidget { color: #00ff00; }</Style> <!-- inline QSS -->
	<MinimumSize>50,50</MinimumSize>        <!-- min width,height -->
	<MaximumSize>200,200</MaximumSize>      <!-- max width,height -->
	<SizePolicy>Fixed,Expanding</SizePolicy> <!-- layout behavior -->
	<Pos>100,50</Pos>                       <!-- absolute x,y -->
	<Size>200,100</Size>                    <!-- width,height -->
</WidgetName>

PushButton - clickable button with states:

<PushButton>
	<Group>[Channel1]</Group>               <!-- control group -->
	<Key>play</Key>                         <!-- control item -->
	<NumberStates>2</NumberStates>          <!-- 2 = on/off, 3+ for multi-state -->
	<State>
		<Number>0</Number>                    <!-- state index (0-based) -->
		<Pressed>btn_play_pressed.svg</Pressed>
		<Unpressed>btn_play_unpressed.svg</Unpressed>
	</State>
	<State>
		<Number>1</Number>
		<Pressed>btn_pause_pressed.svg</Pressed>
		<Unpressed>btn_pause_unpressed.svg</Unpressed>
	</State>
	<Connection>
		<ConfigKey>[Channel1],play</ConfigKey>
		<ButtonState>LeftButton</ButtonState> <!-- LeftButton, RightButton -->
		<EmitOnPressAndRelease>true</EmitOnPressAndRelease>
	</Connection>
</PushButton>

Knob/Slider - continuous value control:

<Knob>
	<Group>[Channel1]</Group>
	<Key>volume</Key>
	<KnobImage>knob_%1.svg</KnobImage>      <!-- %1 = frame number -->
	<BackgroundImage>knob_bg.svg</BackgroundImage>
	<NumberOfFrames>64</NumberOfFrames>     <!-- animation frames -->
	<Connection>
		<ConfigKey>[Channel1],volume</ConfigKey>
	</Connection>
</Knob>

<SliderComposed>
	<Group>[Channel1]</Group>
	<Key>rate</Key>
	<Handle>slider_handle.svg</Handle>
	<Slider>slider_bg.svg</Slider>
	<Horizontal>false</Horizontal>          <!-- vertical slider -->
	<Connection>
		<ConfigKey>[Channel1],rate</ConfigKey>
	</Connection>
</SliderComposed>

Display - show control value as text:

<Label>
	<Text>Channel 1</Text>                  <!-- static text -->
</Label>

<TrackProperty>
	<Group>[Channel1]</Group>
	<Property>title</Property>              <!-- title, artist, album, bpm -->
	<Elide>right</Elide>                    <!-- left, middle, right -->
</TrackProperty>

<NumberRate>
	<Group>[Channel1]</Group>
	<NumberOfDigits>4</NumberOfDigits>
	<Connection>
		<ConfigKey>[Channel1],rate</ConfigKey>
	</Connection>
</NumberRate>

<NumberBpm>
	<Group>[Channel1]</Group>
	<NumberOfDigits>3</NumberOfDigits>
	<Connection>
		<ConfigKey>[Channel1],visual_bpm</ConfigKey>
	</Connection>
</NumberBpm>

Waveform - audio visualization:

<Waveform>
	<Group>[Channel1]</Group>
	<BgColor>#000000</BgColor>
	<SignalColor>#0000ff</SignalColor>
	<BeatColor>#ff0000</BeatColor>
	<PlayPosColor>#00ff00</PlayPosColor>
	<Orientation>horizontal</Orientation>   <!-- horizontal, vertical -->
	<ZoomFactor>3</ZoomFactor>
</Waveform>

<Overview>
	<Group>[Channel1]</Group>
	<BgColor>#202020</BgColor>
	<SignalColor>#0080ff</SignalColor>
	<PlayPosColor>#00ff00</PlayPosColor>
	<MarkerColor>#ffffff</MarkerColor>
</Overview>

Layout - organize widgets:

<WidgetGroup>
	<Layout>horizontal</Layout>              <!-- horizontal, vertical, stacked -->
	<SizePolicy>me,max</SizePolicy>
	<Children>
		<PushButton>...</PushButton>
		<Label>...</Label>
	</Children>
</WidgetGroup>

<WidgetStack>
	<CurrentPage>0</CurrentPage>             <!-- initial page index -->
	<Children>
		<WidgetGroup><!-- page 0 --></WidgetGroup>
		<WidgetGroup><!-- page 1 --></WidgetGroup>
	</Children>
</WidgetStack>

Template - reusable components:

<!-- define template -->
<Template src="skin:templates/deck.xml">
	<SetVariable name="group">[Channel1]</SetVariable>
	<SetVariable name="x">0</SetVariable>
	<SetVariable name="y">0</SetVariable>
</Template>

<!-- templates/deck.xml -->
<Template>
	<WidgetGroup>
		<Pos><Variable name="x"/>,<Variable name="y"/></Pos>
		<PushButton>
			<Group><Variable name="group"/></Group>
			<Key>play</Key>
		</PushButton>
	</WidgetGroup>
</Template>

Singlet - display control via image:

<Singlet>
	<Group>[Channel1]</Group>
	<Key>play</Key>
	<Filename>play_indicator_%1.svg</Filename> <!-- %1 = state -->
	<NumberStates>2</NumberStates>
</Singlet>

VuMeter - level indicator:

<VuMeter>
	<PathVu>vu_%1.svg</PathVu>              <!-- %1 = level 0-100 -->
	<Horizontal>false</Horizontal>
	<NumPixmaps>100</NumPixmaps>            <!-- level steps -->
	<Connection>
		<ConfigKey>[Channel1],VuMeter</ConfigKey>
	</Connection>
</VuMeter>

StatusLight - boolean indicator:

<StatusLight>
	<PathStatusLight>sync_enabled_%1.svg</PathStatusLight>
	<Connection>
		<ConfigKey>[Channel1],sync_enabled</ConfigKey>
	</Connection>
</StatusLight>

Color schemes:

<!-- skin.xml -->
<Schemes>
	<Scheme>
		<Name>Dark</Name>
		<Filters>
			<!-- none = use images as-is -->
		</Filters>
	</Scheme>
	<Scheme>
		<Name>Light</Name>
		<Filters>
			<Invert/>                           <!-- invert colors -->
		</Filters>
	</Scheme>
</Schemes>

Skin manifest (skin.xml):

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE skin>
<skin>
	<manifest>
		<title>My Skin</title>
		<author>Your Name</author>
		<version>1.0.0</version>
		<description>Custom DJ skin</description>
		<language>en</language>
	</manifest>
	
	<attributes>
		<attribute config_key="[Skin],show_4decks" persist="true">0</attribute>
	</attributes>
	
	<Schemes>...</Schemes>
	
	<Style>qss/style.qss</Style>
	
	<WidgetGroup>
		<!-- main layout -->
	</WidgetGroup>
</skin>

QSS styling (style.qss):

#DeckControls {
	background-color: #202020;
	border: 1px solid #404040;
}

WPushButton[value="1"] {
	background-color: #00ff00;  /* active state */
}

WLabel#TrackTitle {
	font-size: 16px;
	font-weight: bold;
	color: #ffffff;
}

best practices:

  • use SVG for resolution independence
  • group related widgets in <WidgetGroup>
  • use templates for repeated patterns (decks, samplers)
  • test with different screen resolutions
  • provide color scheme variants
  • minimize image count (use QSS colors when possible)

Widget Lifecycle and Control Binding

complete widget construction pattern (src/widget/wlabel.cpp):

class WLabel : public QLabel, public WBaseWidget {
		Q_OBJECT
	public:
		WLabel(QWidget* parent = nullptr) 
				: QLabel(parent),
					WBaseWidget(this),
					m_pTextConnection(nullptr) {
				// base initialization only
		}
		
		~WLabel() override {
				// WBaseWidget destructor handles connection cleanup
		}
		
		// called by LegacySkinParser after parsing XML
		void setup(const QDomNode& node, const SkinContext& context) override {
				// parse XML attributes
				QString text = context.selectString(node, "Text");
				if (!text.isEmpty()) {
						setText(text);
				}
				
				// setup control connection from <Connection> tags
				setupConnections(node, context);
		}
		
	protected:
		// called when bound control changes
		void onConnectedControlChanged(double dParameter, double dValue) override {
				Q_UNUSED(dParameter);
				
				// update widget to reflect control value
				if (dValue != 0.0) {
						setStyleSheet("QLabel { color: #00ff00; }");  // active
				} else {
						setStyleSheet("QLabel { color: #808080; }");  // inactive
				}
		}
		
	private:
		ControlParameterWidgetConnection* m_pTextConnection;
};

ControlParameterWidgetConnection lifecycle:

// created by WBaseWidget::setConnection()
class ControlParameterWidgetConnection : public QObject {
	public:
		ControlParameterWidgetConnection(
				WBaseWidget* pWidget,
				ControlProxy* pControl)
				: m_pWidget(pWidget),
					m_pControl(pControl) {
				
				// connect control → widget (forward)
				connect(m_pControl, &ControlProxy::valueChanged,
								this, &ControlParameterWidgetConnection::slotControlValueChanged,
								Qt::QueuedConnection);  // cross-thread safe
				
				// connect widget → control (reverse)
				connect(m_pWidget, &WBaseWidget::valueChanged,
								this, &ControlParameterWidgetConnection::slotWidgetValueChanged,
								Qt::DirectConnection);
		}
		
	private slots:
		void slotControlValueChanged(double value) {
				// notify widget of control change
				m_pWidget->onConnectedControlChanged(
						m_pControl->getParameter(), value);
		}
		
		void slotWidgetValueChanged(double value) {
				// update control from widget
				m_pControl->set(value);
		}
		
	private:
		WBaseWidget* m_pWidget;      // not owned (parent owns)
		ControlProxy* m_pControl;    // owned via QObject parent
};

memory management:

// WBaseWidget manages all connections via Qt object tree
void WBaseWidget::setConnection(ControlProxy* pControl) {
		// create connection (this = parent)
		auto* pConnection = new ControlParameterWidgetConnection(
				this, pControl, this);  // parent = this
		
		m_connections.append(pConnection);
		
		// when WBaseWidget destroyed:
		// 1. Qt deletes all child QObjects (connections)
		// 2. connections' destructors disconnect signals
		// 3. no manual cleanup needed
}

custom widget example (src/widget/wcuemenuitem.cpp):

class WCueMenuItem : public QWidget, public WBaseWidget {
		Q_OBJECT
	public:
		WCueMenuItem(QWidget* parent) 
				: QWidget(parent),
					WBaseWidget(this) {
				// create child widgets
				m_pLabel = new QLabel(this);
				m_pTime = new QLabel(this);
				m_pButton = new QPushButton(this);
				
				// layout
				auto* pLayout = new QHBoxLayout(this);
				pLayout->addWidget(m_pLabel);
				pLayout->addWidget(m_pTime);
				pLayout->addWidget(m_pButton);
				
				// internal signals
				connect(m_pButton, &QPushButton::clicked,
								this, &WCueMenuItem::slotButtonClicked);
		}
		
		void setup(const QDomNode& node, const SkinContext& context) override {
				// bind to hotcue controls
				int hotcueNumber = context.selectInt(node, "HotcueNumber");
				QString group = context.selectString(node, "Group");
				
				// multiple control bindings
				QString positionKey = QString("hotcue_%1_position").arg(hotcueNumber);
				QString activateKey = QString("hotcue_%1_activate").arg(hotcueNumber);
				
				setConnection(new ControlProxy(group, positionKey, this));
				setConnection(new ControlProxy(group, activateKey, this));
		}
		
	protected:
		void onConnectedControlChanged(double dParameter, double dValue) override {
				// determine which control changed
				ControlProxy* pSender = qobject_cast<ControlProxy*>(sender());
				if (pSender->getKey().item.contains("position")) {
						// update time display
						m_pTime->setText(formatTime(dValue));
				} else if (pSender->getKey().item.contains("activate")) {
						// update button state
						m_pButton->setChecked(dValue > 0.0);
				}
		}
		
	private:
		QLabel* m_pLabel;
		QLabel* m_pTime;
		QPushButton* m_pButton;
};

Widget and UI architecture reference:

  • Subsystem overview: High-level Skin/UI subsystem description in Skin/UI with XML parsing
  • Skin development: Official skin creation wiki guide at Creating Skins with examples
  • Control bindings: How widgets connect to controls via ConfigKey explained in Control System
  • Threading: UI event loop and thread safety considerations covered in GUI Thread
  • Waveform widgets: Specialized rendering widgets detailed in Waveform Rendering
  • Code location: src/widget/, src/skin/, src/skin/legacy/ contain widget and skin implementations

History:

  • 0.5 (2002): Hardcoded Qt Designer UI files - limited customization
  • 1.6 (2008): Revolutionary XML skin system - custom interfaces without C++ knowledge
  • 1.7 (2009): WBaseWidget mixin pattern standardizes control binding across widget types
  • 1.8 (2010): ControlParameterWidgetConnection for declarative <Connection> tags
  • 1.9 (2011): Color scheme support within skins
  • 1.11 (2013): Skin template support with <Template> tags - reduced XML duplication
  • 2.0 (2017): SVG image support replaced bitmaps - resolution-independent graphics
  • 2.5.0 (2024): Experimental QML integration via --qml flag - potential XML successor with modern UI capabilities

Preferences and Settings

This section explains Mixxx's configuration management system, showing how UserSettings stores preferences in XML, how the preferences dialog is structured with plugin-style pages, and how settings migration handles version upgrades. Understanding this system is essential for adding new preferences or modifying existing configuration behavior.

Why This Matters: UserSettings uses the same ConfigKey addressing as the Control System, but serves a different purpose—persistent storage vs. real-time communication. Controls can opt-in to persistence via setPersist(true), making their values survive application restarts. The DlgPreferences plugin architecture allows subsystems to add preference pages without modifying core UI code. Settings migration ensures backward compatibility when configuration schema changes.

UserSettings Architecture

UserSettings manages application configuration as key-value pairs stored in XML (~/.mixxx/mixxx.cfg):

class UserSettings : public ConfigObject<ConfigValue> {
	public:
		ConfigValue get(const ConfigKey& key) const;
		void set(const ConfigKey& key, const ConfigValue& value);
		
		// Type-safe accessors
		QString getValueString(const ConfigKey& key, const QString& defaultValue = QString());
		int getValueInt(const ConfigKey& key, int defaultValue = 0);
		bool getValueBool(const ConfigKey& key, bool defaultValue = false);
};

ConfigKey format: ConfigKey("[group]", "item") (same as ControlObject addressing)

Common setting groups:

[Master] - Master output, latency, samplerate
[Sound] - Audio interface configuration
[Controls] - Control value persistence
[Library] - Library view settings, scanner config
[Waveform] - Waveform display settings
[Vinyl] - Vinyl control (DVS) configuration

Settings persistence:

  • Controls can opt-in to save/restore their values
  • `ControlObject::set...

Persist(true)` enables automatic persistence

  • Saved on application shutdown, restored on startup
  • Prevents manual mapping/state loss between sessions

Preference Pages

DlgPreferences manages tabbed settings dialog:

class DlgPreferences : public QDialog {
		// Each tab is a DlgPreferencePage
		void addPageWidget(DlgPreferencePage* pWidget);
		
	private:
		QList<DlgPreferencePage*> m_allPages;
		UserSettingsPointer m_pConfig;
};

DlgPreferencePage base class for preference panes:

class DlgPreferencePage : public QWidget {
	public:
		virtual void slotApply() = 0;      // Apply settings
		virtual void slotUpdate() = 0;     // Load current settings
		virtual void slotResetToDefaults() = 0;  // Reset to defaults
};

Existing preference pages (src/preferences/dialog/):

  • DlgPrefSound - Audio interface configuration
  • DlgPrefLibrary - Library scanner and display options
  • DlgPrefControls - Controller and keyboard mappings
  • DlgPrefWaveform - Waveform display settings
  • DlgPrefEffects - Effects rack configuration
  • DlgPrefEQ - EQ and crossfader settings
  • DlgPrefVinyl - Vinyl control (timecode) settings
  • DlgPrefRecording - Recording directory and format
  • DlgPrefBroadcast - Broadcast streaming settings

Adding new preference page:

  1. Subclass DlgPreferencePage
  2. Create .ui file for layout (Qt Designer)
  3. Implement slotApply(), slotUpdate(), slotResetToDefaults()
  4. Register page in DlgPreferences constructor
  5. Read/write settings via UserSettings pointer

Settings Migration

ConfigVersioning handles preference migrations between Mixxx versions:

// In UserSettings initialization
if (oldVersion < 240) {
		// Migrate settings from 2.3 to 2.4 format
		migrateOldSetting("[Master],enabled", "[Master],master_enabled");
}

Migration strategies:

  • Rename: Move setting to new ConfigKey
  • Transform: Convert value format (e.g., string → enum)
  • Remove: Delete obsolete settings
  • Default: Initialize new settings with sensible defaults

Version tracking:

  • [Config],Version stores current Mixxx version
  • Migration runs automatically on upgrade
  • Prevents data loss during major refactors

Backward compatibility: Mixxx avoids breaking saved mappings/settings when possible.

Preferences and settings architecture reference:

History:

  • 0.5 (2002): Platform-specific settings locations (Windows Registry, KDE configs)
  • 1.6 (2008): Unified UserSettings XML-based system - cross-platform consistency
  • 1.7 (2009): DlgPreferences plugin-style architecture - subsystems register preference dialogs
  • 1.8 (2010): bPersist flag on ControlObjects for automatic control→setting persistence
  • 1.9 (2011): Settings migration and version tracking - handle schema changes without data loss
  • 1.10 (2012): ConfigObject abstraction enabling type-safe access
  • 2.0 (2017): Separate profiles for different user setups
  • 2.2 (2019): DConf integration on Linux for desktop environment integration

Controllers and Scripting

This section dives deep into Mixxx's controller scripting system, explaining how JavaScript code bridges MIDI/HID hardware to Mixxx's controls, the QJSEngine integration, and the connection lifecycle. Understanding these details is essential for developing advanced controller mappings or debugging script behavior.

Why This Matters: The scripting system is what makes Mixxx infinitely extensible—one can map any MIDI/HID device without C++ knowledge. The QJSEngine (Qt JavaScript Engine) provides a full ES6+ environment. Understanding script connection lifecycle prevents memory leaks when disconnecting controllers. The Components.js library demonstrates best practices—study it before writing custom scripts. Soft-takeover prevents parameter jumps when hardware knobs don't match software values.

JavaScript Engine

Controllers can interact with Mixxx via JavaScript:

// Set control values
engine.setValue("[Channel1]", "play", 1);
var isPlaying = engine.getValue("[Channel1]", "play");

// Fractional tempo triggers
var halfBeat = engine.getValue("[Channel1]", "beat_active_0_5");

Script Connections

Connect JavaScript callbacks to control changes:

function onPlayChanged(value, group, key) {
		print("Play state changed to: " + value);
}

engine.connectControl("[Channel1]", "play", onPlayChanged);

Script Connection Implementation

The ScriptConnection class bridges Qt and JavaScript:

class ScriptConnection {
	public:
		ConfigKey key;
		QUuid id;
		QJSValue callback;        // JavaScript function
		// Executes callback when control changes
		void executeCallback(double value) const;
};

MIDI/HID Message Flow

Controller message processing pipeline:

MIDI/HID Device
	↓
Controller Driver (OS)
	↓
PortMIDI / HIDApi
	↓
MidiController / HidController
	↓
processInputMapping() (XML mapping)
	OR
JavaScript callback (script mapping)
	↓
engine.setValue() / ControlProxy
	↓
ControlObject

MidiController processes MIDI messages:

class MidiController : public Controller {
		void receive(const QByteArray& data, mixxx::Duration timestamp);
		
	private:
		// XML-based mapping
		QMultiHash<uint16_t, MidiInputMapping> m_inputMappings;
		
		// Script-based mapping
		ControllerScriptEngineBase* m_pScriptEngine;
};

Input mapping types:

  • Direct: MIDI CC/note → ControlObject (no script)
  • Script: MIDI message → JavaScript callback → complex logic → Controls
  • Soft-takeover: Prevents parameter jumps when controller catches up to software value

Soft-Takeover Algorithm

problem: hardware controller knobs have physical positions that can differ from software parameter values. when a knob is moved, the parameter would jump to the knob's position, causing jarring changes.

example scenario:

software volume: 80%
hardware knob:   20% (user moved it on another layer/deck)
user moves knob to 25% → volume jumps from 80% to 25% (bad!)

solution: ignore hardware input until it "catches up" to the software value.

implementation (src/controllers/softtakeover.cpp):

class SoftTakeoverCtrl {
	public:
		// returns true if input should be ignored
		bool ignore(ControlObject* control, double newParameter) {
				double currentParameter = control->getParameter();
				double distance = fabs(newParameter - currentParameter);
				
				// check if we're in "ignore" mode
				if (m_state == ST_IGNORE) {
						// hardware must cross software value ± threshold
						if ((m_prevParameter <= currentParameter && 
								 newParameter >= currentParameter - kDefaultThreshold) ||
								(m_prevParameter >= currentParameter && 
								 newParameter <= currentParameter + kDefaultThreshold)) {
								// crossed! start tracking
								m_state = ST_TRACK;
								return false;  // allow this value
						}
						// still far away, ignore
						return true;
				}
				
				// track mode: check if jumped too far
				if (distance > kMaxJumpDistance) {
						// parameter changed externally (e.g., preset loaded)
						// enter ignore mode again
						m_state = ST_IGNORE;
						m_prevParameter = newParameter;
						return true;
				}
				
				// normal tracking
				m_prevParameter = newParameter;
				return false;
		}
		
	private:
		enum State { ST_IGNORE, ST_TRACK };
		State m_state = ST_IGNORE;
		double m_prevParameter = 0.0;
		
		static constexpr double kDefaultThreshold = 0.05;  // 5%
		static constexpr double kMaxJumpDistance = 0.5;    // 50%
};

state machine:

Initial state: IGNORE
	↓
Hardware crosses software value ± 5%
	↓
State: TRACK (normal operation)
	↓
Large jump detected (>50%)
	↓
State: IGNORE (software changed externally)

usage in JavaScript:

// enable soft-takeover for a specific control
engine.softTakeover("[Channel1]", "volume", true);

// disable soft-takeover
engine.softTakeover("[Channel1]", "volume", false);

// custom threshold (default is 5%)
engine.softTakeoverThreshold("[Channel1]", "volume", 0.03);  // 3%

when to use: - enable: faders, knobs, potentiometers (continuous controls) - disable: buttons, switches (discrete controls) - automatic: Components.js library handles this for standard controls

visual feedback (optional pattern):

// blink LED when soft-takeover is ignoring
var softTakeoverActive = false;
var blinkTimer = 0;

function handleVolume(channel, control, value, status, group) {
		if (engine.softTakeoverIgnoring(group, "volume")) {
				// ignoring hardware, blink LED
				if (blinkTimer === 0) {
						blinkTimer = engine.beginTimer(250, function() {
								midi.sendShortMsg(status, control, 
										softTakeoverActive ? 127 : 0);
								softTakeoverActive = !softTakeoverActive;
						});
				}
		} else {
				// tracking, stop blinking
				if (blinkTimer !== 0) {
						engine.stopTimer(blinkTimer);
						blinkTimer = 0;
						midi.sendShortMsg(status, control, 127);  // full brightness
				}
				// normal processing
				engine.setValue(group, "volume", script.absoluteLin(value, 0, 1));
		}
}

output (LED feedback):

// Controller script sends LED updates
engine.connectControl("[Channel1]", "play", function(value) {
		midi.sendShortMsg(0x90, 0x10, value ? 127 : 0);  // Note On/Off for LED
});

ControllerEngine JavaScript API:

engine.setValue(group, name, value)        // Set control
engine.getValue(group, name)                // Get control
engine.connectControl(group, name, callback) // Connect callback
engine.beginTimer(millis, callback)        // Periodic callback
midi.sendShortMsg(status, data1, data2)   // Send MIDI

Controllers and scripting architecture reference:

History:

  • 1.0 (2002): MIDI controller support with hardcoded mappings for specific controllers
  • 1.6 (2008): Revolutionary JavaScript scripting engine - controller mappings without C++ knowledge
  • 1.7 (2009): XML mapping format with <input>/<output> tags - separated mapping from script logic
  • 1.9 (2011): Soft takeover prevents parameter jumps
  • 1.11 (2013): HID controller support - non-MIDI USB controllers
  • 2.0+ (2017): Components JS library for reusable code - reduced duplication, hot-swapping without restart
  • 2.4+ (2024): Controller preference dialog improvements - enhanced detection and learning mode

Controller Script API Reference

complete javascript api for controller mapping development.

engine object - core control access:

// control access
engine.getValue(group, key)                  // read control value (double)
engine.setValue(group, key, value)           // write control value
engine.getParameter(group, key)              // read parameter space (0.0-1.0)
engine.setParameter(group, key, param)       // write parameter space

// signal connections
engine.connectControl(group, key, callback)  // connect to valueChanged signal
	// callback: function(value, group, key) { ... }
engine.makeConnection(group, key, callback)  // returns connection object
	connection.trigger()                       // manually trigger callback
	connection.disconnect()                    // remove connection

// soft-takeover
engine.softTakeover(group, key, enable)      // enable/disable soft-takeover
engine.softTakeoverThreshold(group, key, threshold)  // set threshold (0.0-1.0)
engine.softTakeoverIgnoring(group, key)      // check if currently ignoring

// timers
engine.beginTimer(milliseconds, callback)    // start repeating timer, returns ID
engine.beginTimer(milliseconds, callback, oneShot)  // oneShot=true for single shot
engine.stopTimer(timerID)                    // stop timer

// logging
engine.log(message)                          // log to console (deprecated, use console.log)
console.log(message)                         // standard logging
console.warn(message)                        // warning message
console.error(message)                       // error message

// utility
engine.getNumDecks()                         // number of decks (4)
engine.getNumSamplers()                      // number of samplers (64)

midi object - MIDI I/O:

// output
midi.sendShortMsg(status, data1, data2)      // send 3-byte MIDI message
midi.sendSysexMsg(data, length)              // send SysEx (array of bytes)

// utility
midi.noteOn = 0x90                           // constants
midi.noteOff = 0x80
midi.cc = 0xB0

script object - script-level operations:

script.absoluteLin(value, min, max)          // map absolute MIDI (0-127) to range
script.absoluteLinInverse(value, min, max)   // reverse mapping

script.absoluteNonLin(value, min, max, exponent)  // non-linear mapping
script.midiPitch(LSB, MSB, min, max)         // combine pitch bend bytes

script.spinback(group, activate, rate, interval)  // spinback effect
script.brake(group, activate, rate, interval)     // brake effect

script.crossfaderCurve(value, min, max)      // apply crossfader curve
script.softTakeover = true                   // enable soft-takeover globally

components library (reusable patterns):

// button component
var playButton = new components.Button({
		midi: [0x90, 0x10],          // note on, note 0x10
		group: '[Channel1]',
		key: 'play',
		type: components.Button.prototype.types.toggle,
		outKey: 'play_indicator',    // LED feedback
		on: 127,                     // brightness when on
		off: 0                       // brightness when off
});

playButton.input = function(channel, control, value, status, group) {
		// custom input handler
		this.inSetValue(value);
};

playButton.output = function(value, group, control) {
		// custom output handler  
		this.send(value ? this.on : this.off);
};

// pot component (knob/slider)
var volumeKnob = new components.Pot({
		midi: [0xB0, 0x20],          // CC channel 1, control 0x20
		group: '[Channel1]',
		key: 'volume',
		max: 1.0,
		min: 0.0,
		invert: false                // reverse direction
});

// encoder component (endless rotary)
var browseEncoder = new components.Encoder({
		midi: [0xB0, 0x30],
		group: '[Library]',
		key: 'MoveVertical',
		input: function(channel, control, value, status, group) {
				// value > 64 = clockwise, value < 64 = counter-clockwise
				var direction = value > 64 ? 1 : -1;
				engine.setValue(this.group, this.key, direction);
		}
});

// component container (deck/effect unit)
var deck = new components.Deck([1, 2]);  // decks 1 and 2

deck.playButton = new components.Button({
		midi: [0x90, 0x10],
		key: 'play'
});

deck.reconnectComponents(function(component) {
		// called for each component
		component.group = '[Channel1]';  // override group
});

deck.shutdown = function() {
		// cleanup LEDs
		this.playButton.send(0);
};

controller lifecycle callbacks:

// required: initialization
var MyController = {};

MyController.init = function(id, debugging) {
		// called when controller connected
		// id = controller MIDI ID
		// debugging = true if controller debugging enabled
		
		// initialize state
		MyController.deck = new components.Deck([1, 2]);
		
		// setup components
		MyController.deck.playButton.midi = [0x90, 0x10];
		
		// initial LED state
		MyController.deck.playButton.send(0);
};

// required: cleanup
MyController.shutdown = function() {
		// called when controller disconnected or Mixxx quits
		
		// turn off all LEDs
		for (var i = 0; i < 128; i++) {
				midi.sendShortMsg(0x90, i, 0);
		}
};

// optional: incoming data handler
MyController.incomingData = function(data, length) {
		// handle raw MIDI/HID data
		// useful for SysEx or complex messages
};

common patterns:

led feedback:

function updateLED(value, group, key) {
		var status = 0x90;  // note on
		var control = 0x10;  // note number
		var brightness = value ? 127 : 0;
		midi.sendShortMsg(status, control, brightness);
}

engine.connectControl('[Channel1]', 'play', updateLED);

shift button modifier:

var shifted = false;

function shiftButton(channel, control, value, status, group) {
		shifted = (value > 0);  // pressed
}

function playButton(channel, control, value, status, group) {
		if (value === 0) return;  // ignore button release
		
		if (shifted) {
				engine.setValue('[Channel1]', 'cue_default', 1);  // shift+play = cue
		} else {
				engine.setValue('[Channel1]', 'play', !engine.getValue('[Channel1]', 'play'));
		}
}

parameter smoothing:

var targetValue = 0;
var currentValue = 0;
var smoothTimer = 0;

function setParameterSmooth(group, key, target) {
		targetValue = target;
		
		if (smoothTimer === 0) {
				smoothTimer = engine.beginTimer(20, function() {
						var diff = targetValue - currentValue;
						currentValue += diff * 0.3;  // 30% per step
						
						engine.setValue(group, key, currentValue);
						
						if (Math.abs(diff) < 0.001) {
								engine.stopTimer(smoothTimer);
								smoothTimer = 0;
						}
				});
		}
}

beat-synced actions:

function beatJump(group, beats) {
		var bpm = engine.getValue(group, 'bpm');
		var sampleRate = 44100;
		var currentPos = engine.getValue(group, 'playposition');
		var duration = engine.getValue(group, 'duration');
		
		var samplesPerBeat = (60.0 / bpm) * sampleRate * 2;  // stereo
		var jumpSamples = beats * samplesPerBeat;
		var jumpPosition = currentPos + (jumpSamples / (duration * sampleRate * 2));
		
		engine.setValue(group, 'playposition', jumpPosition);
}

debugging helpers:

// dump all control values for a group
function dumpControls(group) {
		var controls = ['play', 'bpm', 'volume', 'rate', 'sync_enabled'];
		for (var i = 0; i < controls.length; i++) {
				var value = engine.getValue(group, controls[i]);
				console.log(group + ',' + controls[i] + ' = ' + value);
		}
}

// log MIDI messages
function logMIDI(channel, control, value, status, group) {
		console.log('MIDI: ch=' + channel + ' ctrl=' + control.toString(16) + 
								' val=' + value + ' status=' + status.toString(16));
}

Special Features

This section covers Mixxx's specialized DJ features including Digital Vinyl System (DVS) for turntable control, recording and broadcasting capabilities, and Auto DJ for automated mixing. These advanced features distinguish Mixxx as a professional DJ platform.

Why This Matters: DVS (vinyl control) requires real-time signal processing to decode timecode from audio input—any latency ruins the tactile vinyl feel. Recording and broadcasting must not block the audio thread. Auto DJ's transition detection and crossfade logic demonstrate sophisticated playlist management. Understanding these features shows how Mixxx integrates complex workflows into its architecture.

Vinyl Control (DVS)

Digital Vinyl System allows DJs to control Mixxx using turntables/CDJs with timecode vinyl/CDs.

VinylControlManager (.cpp) coordinates vinyl control for all decks:

class VinylControlManager : public QObject {
		// Creates VinylControl for each deck with vinyl input
		void addSignalQualityListener(VinylSignalQualityListener* pListener);
		void toggleVinylControl(const QString& group);
};

VinylControl (.cpp) per-deck timecode processing:

class VinylControl : public QObject {
		// Analyzes timecode signal (Serato CV02, Traktor MK2, MixVibes DVS)
		// Extracts: Position, speed, direction from audio signal
		// Controls: [ChannelN],vinylcontrol_* controls
		
	private:
		VinylControlProcessor* m_pProcessor;  // DSP for timecode detection
		ControlProxy* m_pControlVinylSeek;    // Output: seek position
		ControlProxy* m_pControlVinylRate;    // Output: playback rate
};

Timecode formats supported:

  • Serato CV02: Industry standard (Serato Scratch Live/DJ)
  • Traktor MK2: Native Instruments Traktor Scratch
  • MixVibes DVS: MixVibes Cross timecode

Signal quality indicators:

  • Green: Strong signal, accurate tracking
  • Yellow: Weak signal, potential skips
  • Red: No signal or unreadable

Vinyl control modes:

  • Absolute: Needle position = track position (like real vinyl)
  • Relative: Speed/direction only, maintains cue points
  • Constant: Fixed playback, vinyl only controls scratch effects

Configuration (DlgPrefVinyl):

  • Input routing: Which audio input receives timecode
  • Timecode type selection
  • Lead-in time (silence before timecode starts)
  • Signal quality threshold

Code location: src/vinylcontrol/ contains DVS implementation Preferences: Configuration details in Preferences and Settings

Recording and Broadcasting

EngineRecord (.cpp) captures master output to audio file:

class EngineRecord : public EngineObject {
		// Records master output to WAV/FLAC/MP3/OGG
		void process(CSAMPLE* pInOut, const std::size_t bufferSize) override;
		
	private:
		EncoderPointer m_pEncoder;  // Audio encoder (LAME, libvorbis, etc.)
		QFile m_outputFile;
};

Recording controls:

[Recording],status           // 0=stopped, 1=recording
[Recording],toggle_recording // Start/stop recording
[Recording],quality          // Bitrate/quality setting

Recording configuration:

  • Output directory (DlgPrefRecording)
  • File format: WAV (lossless), FLAC (compressed lossless), MP3, OGG Vorbis
  • Quality/bitrate settings
  • Split recording on track change (optional)

EngineBroadcast (.cpp) streams to Icecast/Shoutcast servers:

class EngineBroadcast : public QThread {
		// Encodes master output and streams to server
		void run() override;  // Thread sends data to streaming server
		
	private:
		QList<BroadcastProfile*> m_profiles;  // Multiple server configs
		EncoderPointer m_pEncoder;
};

Broadcasting controls:

[Broadcast],enabled          // Connect/disconnect from server
[Broadcast],status           // Connection status

Broadcasting configuration (DlgPrefBroadcast):

  • Server URL, port, mount point
  • Stream format: MP3, OGG Vorbis
  • Bitrate, sample rate
  • Stream metadata (DJ name, show title)
  • Multiple server profiles (stream to multiple destinations)

Metadata updates: Track changes automatically update stream metadata (Now Playing).

Code location: src/recording/, src/broadcast/ contain recording and streaming implementations Audio processing: Integration with engine pipeline detailed in Engine and Audio Processing

Auto DJ

Auto DJ automatically loads and plays tracks from a playlist for continuous playback.

AutoDJProcessor (.cpp) manages automatic track transitions:

class AutoDJProcessor : public QObject {
		// Monitors deck state, loads next track when current ends
		void playerPositionChanged(mixxx::audio::FramePos position);
		void fadeNow();  // Trigger fade transition
		
	private:
		int m_iAutoDJPlaylistId;  // Playlist being auto-played
		double m_transitionTime;   // Fade duration
		AutoDJProcessor::Mode m_mode;  // Full intro, fade at end, etc.
};

Auto DJ controls:

[AutoDJ],enabled             // Enable/disable Auto DJ
[AutoDJ],fade_now            // Trigger immediate transition
[AutoDJ],skip_next           // Skip to next track
[AutoDJ],shuffle_playlist    // Randomize queue

Transition modes:

  • Full Intro: Crossfade during intro section
  • Fade at Outro: Fade out current, fade in next near end
  • Full Outro: Wait until track fully ends
  • Skip Silence: Detect and skip silent sections
  • Skip Silence, Start with Xfader centered (2.6.0): Skip silence and start new track with crossfader in center position

Auto DJ playlist:

  • Special "Auto DJ" playlist in library
  • Drag tracks to add to queue
  • Tracks removed after playing (optional)
  • Random mode shuffles queue

Transition timing:

  • Uses track's outro start marker (if available)
  • Configurable fade duration (1-20 seconds)
  • Respects BPM for smooth transitions (optional beatmatching)

Use cases:

  • Radio shows with minimal intervention
  • Bar/restaurant background music
  • Podcast recording with pre-selected tracks
  • Practice beatmatching against Auto DJ

Special features architecture reference:

History (Mixer, Engine, CHANGELOG.md):

Auto DJ Evolution:

  • 1.x series (2008-2011): Auto DJ introduced with basic fade transitions

  • 1.8 (2010): Configurable transition time, intro/outro detection

  • 1.11 (2014): Improved transition modes (full intro, fade at outro)

  • 2.0 (2015-2017): Skip silence mode introduced, playlist management improvements

  • 2.1-2.3 (2018-2020): Random mode enhancements, transition timing refinements

  • 2.4 (2024): Auto DJ stability improvements, better track queue handling

  • 2.5 (2024-12): UI improvements, better integration with library search

  • 2.6.0 (main branch): Skip silence with centered crossfader, auto-recenter option, context menu control

    • Skip Silence mode: New transition mode "Skip Silence, Start with Xfader centered" for automatic silence detection
    • Crossfader recenter option: Optional automatic crossfader recentering when turning off Auto DJ (default: off)
    • Context menu action: Enable/disable Auto DJ directly from context menu
  • Audio processing: Engine and Audio Processing - Real-time requirements for DVS

  • Code location: src/vinylcontrol/, src/recording/, src/library/autodj/


Build System

This section explains Mixxx's CMake-based build system (Platform Requirements, Code Organization), covering feature flags, dependency management via vcpkg, and platform-specific configurations. Understanding the build system is essential for adding new features or debugging compilation issues.

Why This Matters: CMake's AUTOMOC setting (Qt Meta-Object System, Qt Integration Patterns) automatically processes Q_OBJECT macros—forgetting this for new classes causes cryptic linker errors (Anti-Patterns). Feature flags like MODPLUG (Audio Infrastructure), LILV (Effects System, LV2 Backend), QTKEYCHAIN control optional dependencies (Core Libraries). vcpkg provides reproducible cross-platform builds. Understanding the build system helps debug "works on my machine" issues and enables adding new external dependencies properly (Debugging Strategies).

CMake Configuration

Top-level CMakeLists.txt (/CMakeLists.txt, Code Organization):

cmake_minimum_required(VERSION 3.20)  # Requires modern CMake features
project(mixxx VERSION 2.6.0 LANGUAGES C CXX)  # Version from main branch

# C++ standard (C++20 required since 2.5.0, *[Platform Requirements](#platform-specific-details)*)
set(CMAKE_CXX_STANDARD 20)  # C++20 features: concepts, ranges, modules support
set(CMAKE_CXX_STANDARD_REQUIRED ON)  # Enforce C++20, fail if unavailable
set(CMAKE_CXX_EXTENSIONS OFF)  # No GNU extensions, portable ISO C++

# Build type (Debug, Release, RelWithDebInfo, MinSizeRel)
if(NOT CMAKE_BUILD_TYPE)
		set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING "Build type" FORCE)
endif()
# Debug: -O0 -g (no optimization, full debug symbols)
# Release: -O3 -DNDEBUG (max optimization, no asserts)
# RelWithDebInfo: -O2 -g (optimization + debug symbols) ← default

# Qt integration (critical for Mixxx, *[Qt Integration Patterns](#qt-integration-patterns)*)
set(CMAKE_AUTOMOC ON)  # Automatically run MOC on Q_OBJECT headers (*[Qt Meta-Object System](#qt-integration-patterns)*)
set(CMAKE_AUTORCC ON)  # Process .qrc resource files
set(CMAKE_AUTOUIC OFF)  # Don't use Qt Designer .ui files

find_package(Qt6 6.2 REQUIRED COMPONENTS  # Qt6 default since 2.5.0 (*[Core Libraries](#core-libraries)*)
		Core        # QObject, QString, QList, etc.
		Widgets     # QWidget, QMainWindow, QPushButton (*[Skin/UI](#skinui-srcskin-srcwidget)*)
		Sql         # QSqlDatabase, QSqlQuery (*[Library](#library-srclibrary)*)
		OpenGL      # Waveform rendering (*[Waveform Rendering](#waveform-rendering)*)
		Svg         # SVG image support
		Xml         # Skin XML parsing (*[LegacySkinParser](#skinui-srcskin-srcwidget)*)
)

# Platform detection (*[Platform Requirements](#platform-specific-details)*)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")  # Ubuntu 22.04+, GCC 11+ / Clang 14+
		set(LINUX ON)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")  # macOS 11+, Xcode 13+
		set(APPLE ON)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")  # Windows 10 build 1809+, VS 2019 16.11+
		set(WIN32 ON)
endif()

Adding source files (Code Organization):

# Pattern 1: Explicit list (preferred for control)
target_sources(mixxx PRIVATE
		src/control/controlobject.cpp  # *[Control System](#control-system-srccontrol)*
		src/control/controlobject.h
		src/control/controlproxy.cpp   # *[ControlProxy](#control-hierarchy)*
		src/control/controlproxy.h
)

# Pattern 2: Glob (convenient but can miss new files)
file(GLOB ENGINE_SOURCES src/engine/*.cpp src/engine/*.h)  # *[Engine](#engine-srcengine)*
target_sources(mixxx PRIVATE ${ENGINE_SOURCES})
# Warning: Glob doesn't auto-detect new files, requires CMake reconfigure

# Pattern 3: Object library (for large subsystems)
add_library(mixxx_library OBJECT
		src/library/trackcollection.cpp  # *[Library](#library-srclibrary)*
		src/library/dao/trackdao.cpp     # *[TrackDAO](#library-srclibrary)*
		# ... more files ...
)
target_link_libraries(mixxx PRIVATE mixxx_library)
# Benefit: Parallel compilation, modular organization (*[Performance Characteristics](#performance-characteristics)*)

Custom MOC processing (for special cases, Qt Meta-Object System):

# Normally AUTOMOC handles this, but for non-standard cases:
qt_wrap_cpp(CONTROL_STRING_MOC_SOURCES
		src/control/controlstring.h  # *[Control System](#control-system-srccontrol)*
		OPTIONS -DMOC_EXTRA_DEFINE  # Pass defines to MOC
)
target_sources(mixxx PRIVATE ${CONTROL_STRING_MOC_SOURCES})
# Generates: moc_controlstring.cpp
# Why needed: controlstring.h has special requirements

# Qt resources (images, skins, fonts)
qt_add_resources(RESOURCE_SOURCES
		res/mixxx.qrc  # Lists all embedded resources
)
target_sources(mixxx PRIVATE ${RESOURCE_SOURCES})
# Generates: qrc_mixxx.cpp with binary data embedded

Compiler warnings (strict by default):

if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
		target_compile_options(mixxx PRIVATE
				-Wall           # Enable most warnings
				-Wextra         # Enable extra warnings
				-Wpedantic      # ISO C++ compliance warnings
				-Werror         # Treat warnings as errors (CI builds)
				-Wno-unused-parameter  # Too noisy, disabled
		)
elseif(MSVC)
		target_compile_options(mixxx PRIVATE
				/W4             # Warning level 4 (high)
				/WX             # Treat warnings as errors
		)
endif()

Link-time optimization (LTO):

if(CMAKE_BUILD_TYPE STREQUAL "Release")
		set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)  # Enable LTO
		# Benefits: 5-10% smaller binary, 2-5% faster
		# Cost: Much slower linking (minutes instead of seconds)
endif()

Feature Flags

Optional dependencies (configure with cmake -DMODPLUG=ON, Platform Requirements):

option(MODPLUG "Enable MOD/XM/IT tracker file support" OFF)  # *[Audio File Formats](#audio-file-formats-and-extensions)*
option(LILV "Enable LV2 plugin support" ON)  # *[Effects System](#effects-system)*, *[LV2 Backend](#effects-system)*
option(QTKEYCHAIN "Enable secure credential storage" ON)  # *[Recording and Broadcasting](#recording-and-broadcasting)*
option(FFMPEG "Enable FFmpeg for additional codecs" ON)  # *[Audio Infrastructure](#core-libraries)*
option(BULK "Enable USB bulk controller support" ON)  # *[Controllers](#controllers-srccontrollers)*
option(HID "Enable HID controller support" ON)  # *[HID Support](#controllers-srccontrollers)*
option(OPUS "Enable Opus broadcast codec" ON)  # *[Recording and Broadcasting](#recording-and-broadcasting)*
option(FAAD "Enable AAC decoding via FAAD" OFF)  # Use FFmpeg instead
option(LOCALECOMPARE "Use locale-aware string comparison" ON)

# Example usage:
if(MODPLUG)  # *[Audio File Formats](#audio-file-formats-and-extensions)*
		find_package(libopenmpt REQUIRED)  # *[Core Libraries](#core-libraries)*
		target_link_libraries(mixxx PRIVATE libopenmpt::libopenmpt)
		target_compile_definitions(mixxx PRIVATE __MODPLUG__)
		# Enables: src/sources/soundsourcemodplug.cpp (*[SoundSource](#audio-infrastructure)*)
endif()

if(LILV)  # *[Effects System](#effects-system)*
		find_package(Lilv REQUIRED)  # LV2 plugin host library (*[LILV](#core-libraries)*)
		target_link_libraries(mixxx PRIVATE Lilv::Lilv)
		target_compile_definitions(mixxx PRIVATE __LILV__)
		# Enables: src/effects/lv2/lv2backend.cpp (*[LV2 Backend](#effects-system)*)
endif()

if(QTKEYCHAIN)  # *[Recording and Broadcasting](#recording-and-broadcasting)*
		find_package(Qt6Keychain REQUIRED)
		target_link_libraries(mixxx PRIVATE Qt6Keychain::Qt6Keychain)
		target_compile_definitions(mixxx PRIVATE __QTKEYCHAIN__)
		# Enables: Secure storage for broadcast passwords (*[Configuration Files](#configuration-files)*)
endif()

Using feature flags in code (Coding Style Essentials):

// Check at compile time:
#ifdef __MODPLUG__
		#include "sources/soundsourcemodplug.h"  // *[Audio File Formats](#audio-file-formats-and-extensions)*
		// Register MOD decoder (*[SoundSource](#audio-infrastructure)*)
		SoundSourceProviderRegistry::instance()->registerProvider(
				std::make_shared<SoundSourceProviderModPlug>());
#endif

// Or use constexpr:
#ifdef __LILV__
		constexpr bool kLilvEnabled = true;
#else
		constexpr bool kLilvEnabled = false;
#endif

if constexpr (kLilvEnabled) {  // *[Effects System](#effects-system)*
		// Code eliminated at compile time if LILV disabled
		loadLV2Plugins();  // *[LV2 Backend](#effects-system)*
}

Debug vs Release differences (Debugging Strategies, Coding Style Essentials):

// Assertions (only in Debug builds, *[Anti-Patterns](#anti-patterns)*):
#ifndef NDEBUG
		#define DEBUG_ASSERT(condition) assert(condition)
		#define DEBUG_ASSERT_AND_HANDLE(condition) assert(condition)
#else
		#define DEBUG_ASSERT(condition)
		#define DEBUG_ASSERT_AND_HANDLE(condition) if (!(condition))
#endif

// Example:
DEBUG_ASSERT(bufferSize > 0);  // Crash in Debug, ignored in Release
DEBUG_ASSERT_AND_HANDLE(pTrack) {  // Crash in Debug, handle in Release (*[Track Pointer Pattern](#track-pointer-pattern)*)
		return;  // Graceful fallback in Release
}

// Logging levels (*[Debugging Strategies](#debugging-strategies)*):
#ifndef NDEBUG
		qDebug() << "Detailed debug info";  // Compiled in Debug only
#endif
qWarning() << "Warning message";  // Always compiled

Platform-specific defines:

if(LINUX)
		target_compile_definitions(mixxx PRIVATE
				__LINUX__
				_FILE_OFFSET_BITS=64  # Large file support
		)
		target_link_libraries(mixxx PRIVATE
				dl          # Dynamic linking
				pthread     # POSIX threads
				X11         # X Window System
		)
elseif(APPLE)
		target_compile_definitions(mixxx PRIVATE
				__APPLE__
		)
		target_link_libraries(mixxx PRIVATE
				"-framework CoreFoundation"
				"-framework CoreAudio"
				"-framework AudioToolbox"
		)
elseif(WIN32)
		target_compile_definitions(mixxx PRIVATE
				__WINDOWS__
				UNICODE           # Use wide char APIs
				_UNICODE
				NOMINMAX          # Don't define min/max macros
		)
		target_link_libraries(mixxx PRIVATE
				ws2_32        # Windows Sockets 2
				winmm         # Windows Multimedia
		)
endif()

Dependencies and vcpkg

vcpkg manifest (vcpkg.json):

{
	"name": "mixxx",
	"version-string": "2.6.0",  # Development version
	"dependencies": [
		"qt6-base",
		"qt6-svg",
		"qt6-tools",
		"portaudio",
		"soundtouch",
		"rubberband",
		"chromaprint",
		"libid3tag",
		"taglib",
		"flac",
		"opus",
		"libvorbis",
		"libebur128"
	],
	"overrides": [
		{ "name": "qt6-base", "version>=" : "6.5.0" }
	]
}

Building with vcpkg:

# Install vcpkg:
git clone https://github.com/microsoft/vcpkg.git
cd vcpkg && ./bootstrap-vcpkg.sh

# Configure Mixxx to use vcpkg:
cmake -B build \
	-DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake
# vcpkg automatically installs dependencies from vcpkg.json

# Build:
cmake --build build --parallel 8
# Uses 8 CPU cores for parallel compilation

Dependency resolution (how vcpkg works):

  1. CMake reads vcpkg.json manifest
  2. vcpkg downloads source for each dependency
  3. vcpkg builds dependencies with same compiler/flags as Mixxx
  4. vcpkg installs to vcpkg_installed/<triplet>/ directory
  5. CMake find_package() automatically finds vcpkg packages

Binary cache (speed up repeated builds):

# Use binary cache to avoid recompiling dependencies:
export VCPKG_BINARY_SOURCES="clear;files,/home/user/.vcpkg-cache,readwrite"
# First build: Downloads/builds dependencies, caches binaries
# Subsequent builds: Reuses cached binaries (~10x faster)

Compilation Process

Build steps (what happens during cmake --build):

1. MOC Generation (~5 seconds)
	 - Scans all headers for Q_OBJECT macro
	 - Generates moc_*.cpp files with Qt meta-object code
	 - ~200 MOC files generated for Mixxx

2. Source Compilation (~2-10 minutes)
	 - Compiles all .cpp files to .o object files
	 - ~1500 source files in Mixxx
	 - Parallel compilation: 8 cores = ~2 min, 1 core = ~15 min
	 - ccache speeds up recompilation: ~80-95% cache hit rate

3. Linking (~10-60 seconds)
	 - Links all .o files into final mixxx executable
	 - Links external libraries (Qt, PortAudio, etc.)
	 - Debug build: ~10 sec, Release with LTO: ~60 sec

4. Resource Embedding (~2 seconds)
	 - Compiles qrc_mixxx.cpp (embedded skins, images, fonts)
	 - ~10MB of resources embedded in binary

Incremental builds (only recompile changed files):

# Change one .cpp file:
touch src/control/controlobject.cpp
cmake --build build
# Recompiles: controlobject.cpp → controlobject.o
# Relinks: mixxx executable
# Time: ~5 seconds

# Change one .h file with Q_OBJECT:
touch src/control/controlobject.h
cmake --build build
# Recompiles: 
#   - controlobject.cpp (includes the .h)
#   - All files that include controlobject.h (~50 files)
#   - moc_controlobject.cpp (regenerated)
# Relinks: mixxx executable
# Time: ~30 seconds

Compilation database (compile_commands.json):

# Generate for IDE/tools (clangd, cppcheck, clang-tidy):
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
# Creates build/compile_commands.json with all compiler invocations
# Used by: VS Code, CLion, clangd, clang-tidy

ccache (compiler cache for faster rebuilds):

# Install ccache:
sudo apt install ccache  # Linux
brew install ccache      # macOS

# Configure CMake to use ccache:
cmake -B build -DCMAKE_CXX_COMPILER_LAUNCHER=ccache

# Stats:
ccache -s
# cache hit rate:  85%    ← 85% of compilations served from cache
# Speedup: 10-20x for full rebuilds after git checkout

Platform-Specific Details

Platform Requirements (updated in 2.5.0):

  • Linux: Ubuntu 22.04+ (or equivalent: Debian 12+, Fedora 36+, Arch current)
    • Compiler: GCC 11+ or Clang 14+ (C++20 support required)
    • Reason: Qt6 and C++20 standard library requirements
  • macOS: macOS 11 (Big Sur) or later
    • Architecture: Universal binaries (x86_64 + arm64 Apple Silicon)
    • Xcode: 13.0+ for C++20 support
  • Windows: Windows 10 build 1809 or later (October 2018 Update)
    • Compiler: Visual Studio 2019 16.11+ or Visual Studio 2022
    • Reason: C++20 standard library and Qt6 compatibility

Linux (Ubuntu/Debian example):

# Install dependencies (Ubuntu 22.04+):
sudo apt install build-essential cmake qt6-base-dev \
	libportaudio2 libsoundtouch-dev librubberband-dev \
	libchromaprint-dev libtag1-dev libflac++-dev

# Verify C++20 compiler:
g++ --version  # Should be GCC 11+
# or
clang++ --version  # Should be Clang 14+

# Build:
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel $(nproc)

# Install:
sudo cmake --install build
# Installs to: /usr/local/bin/mixxx

macOS (using Homebrew, macOS 11+):

# Install dependencies:
brew install cmake qt@6 portaudio soundtouch rubberband \
	chromaprint taglib flac opus vorbis

# Verify macOS version:
sw_vers  # Should show macOS 11.0 or later

# Build universal binary (x86_64 + arm64):
cmake -B build \
	-DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" \
	-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0  # Minimum: macOS 11
cmake --build build --parallel $(sysctl -n hw.ncpu)

# Create app bundle:
cmake --build build --target dmg
# Generates: build/Mixxx-2.6.0-universal.dmg  # Development build

Windows (using vcpkg, Windows 10 build 1809+):

# Install Visual Studio 2022 with C++ workload
# Minimum: VS 2019 16.11, Recommended: VS 2022

# Verify Windows build:
winver  # Should show build 1809 (17763) or later

# Build (use vcpkg for dependencies):
cmake -B build -G "Visual Studio 17 2022" -A x64 `
	-DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake
cmake --build build --config Release --parallel

# Run:
.\build\Release\mixxx.exe

Build system architecture reference:

History:

  • 2001: Hand-written Makefiles for building
  • 2005: Transition to SCons (Python-based) - better cross-platform support
  • 2020-2023: Revolutionary migration to CMake during 2.4 development
    • AUTOMOC support eliminates manual moc invocations
    • vcpkg integration solves Windows "dependency hell"
    • Out-of-source builds enforced
    • CTest integration for automated testing
    • Compile times dropped from 20+ minutes to under 5 minutes with ccache
    • Pre-commit hooks for clang-format
    • Build parallelization improved from 1 core to full multi-core
    • Feature flags (MODPLUG, LILV, QTKEYCHAIN, FFMPEG) for optional dependencies

Integration Examples

This section provides practical, copy-paste-ready examples for common integration tasks in Mixxx (Extension Points, Adding New Features: Quick Checklist), from adding simple deck features to implementing bidirectional controller feedback. These examples demonstrate the architectural patterns (Key Architectural Patterns) in action and serve as templates for new development.

Why This Matters: Reading architecture documentation is one thing—seeing working code is another. These examples show correct patterns: control creation lifecycle (Control System), thread-safe library access (Track Pointer Pattern), proper signal/slot connections (Signal/Slot Connection Patterns). The checklist prevents common mistakes (forgetting persistence, wrong connection types, missing MOC processing, Anti-Patterns). Study these examples before starting new features—they encode years of best practices and avoid common pitfalls (Code Review Best Practices.

Adding New Features: Quick Checklist

Control-Based Feature Checklist (Control System, Extension Points):

  1. Define ConfigKey: Choose appropriate group and item names (use snake_case, ConfigKey)
  2. Create ControlObject: In the appropriate owner class constructor (Creating Controls)
  3. Persistence: Set bPersist=true if the value should be saved between sessions (Configuration Files)
  4. Audio Thread Access: Use ControlProxy if accessed from audio processing (Control Access Patterns, ControlProxy)
  5. Timing: Connect to EngineControl::process() if timing-dependent (Audio Buffer Processing, EngineControl)
  6. Controller Access: Expose via JavaScript API if controllers need access (MIDI Scripting, Controllers)
  7. UI Integration: Add to skin XML if a widget is needed (Skin/UI, Creating Skins)
  8. Testing: Write unit tests using MixxxTest fixtures (Testing Infrastructure, Test Base Classes)

Thread Safety Checklist (Thread Safety Considerations, Threading Model):

Related documentation: Control creation fundamentals in Control System and troubleshooting in Debugging Strategies

Basic Integration Pattern

General template for adding new controls (Control System, Creating Controls):

// 1. Create control in class constructor
m_pControlNew = new ControlObject(
		ConfigKey("[Channel1]", "new_feature"),  // *[ConfigKey](#configkey-universal-addressing)*
		true,   // ignore no-op sets
		false,  // don't track statistics
		false,  // don't persist (*[Configuration Files](#configuration-files)*)
		0.0);   // default value

// 2. Set description for tooltips and documentation
m_pControlNew->setDescription(
		tr("Description of what this control does"));  // Translated string

// 3. Connect signal if you need to respond to changes (*[Signal/Slot Connection Patterns](#signalslot-connection-patterns)*)
connect(m_pControlNew, &ControlObject::valueChanged,
				this, &MyClass::slotNewFeature,
				Qt::DirectConnection);  // or AutoConnection for UI (*[Qt Integration Patterns](#qt-integration-patterns)*)

// 4. Implement handler
void MyClass::slotNewFeature(double value) {
		// Respond to control changes (*[Observer Pattern](#key-architectural-patterns)*)
}

Pattern reference: Control implementation in Control System Details and extensibility in Extension Points

Example 1: Adding a Simple Deck Feature

Goal: Add a "loop_double" feature that doubles the current loop size.

Step 1 - Create control in LoopingControl constructor:

m_pLoopDouble = new ControlPushButton(ConfigKey(group, "loop_double"));
connect(m_pLoopDouble, &ControlObject::valueChanged,
				this, &LoopingControl::slotLoopDouble,
				Qt::DirectConnection);

Step 2 - Implement the slot:

void LoopingControl::slotLoopDouble(double pressed) {
		if (pressed <= 0.0) return;  // Button release
		if (!m_pLoopEnabled->toBool()) return;  // No active loop
		
		double currentSize = m_pLoopEndPosition->get() - m_pLoopStartPosition->get();
		setLoop(m_pLoopStartPosition->get(), 
						m_pLoopStartPosition->get() + currentSize * 2.0);
}

Implementation reference: Control fundamentals in The Control System and audio processing in Engine and Audio Processing

Example 2: Bidirectional Controller LED Feedback

Goal: LED tracks play state with automatic updates.

YourController.init = function() {
		engine.connectControl("[Channel1]", "play", YourController.onPlayChanged);
		YourController.setLED(YourController.playButton, engine.getValue("[Channel1]", "play"));
};

YourController.onPlayChanged = function(value, group, control) {
		YourController.setLED(YourController.playButton, value ? 0x7F : 0x00);
};

YourController.playPress = function(channel, control, value, status, group) {
		if (value) {
				engine.setValue(group, "play", !engine.getValue(group, "play"));
		}
};

Controller reference: JavaScript integration patterns in Controller and Scripting Integration

Example 3: Thread-Safe Library Access

Wrong ❌:

void MyEngineControl::process(...) {
		// NEVER: Causes glitches!
		double bpm = m_pLoadedTrack->getBpm();  // Potential lock
}

Correct ✅:

void MyEngineControl::trackLoaded(TrackPointer pNewTrack) {
		if (pNewTrack) {
				m_pBpmControl->set(pNewTrack->getBpm());  // Cache in atomic control
		}
}

void MyEngineControl::process(...) {
		double bpm = m_pBpmControl->get();  // Lock-free read
}

Related patterns: Memory ownership patterns in Memory Management and pitfalls in Anti-Patterns


Debugging and Troubleshooting

This section provides practical debugging strategies for common Mixxx development issues, from control connection problems to audio glitches to thread safety violations. These solutions come from real-world experience debugging the architecture patterns described throughout this guide.

Why This Matters: Mixxx's multi-threaded, real-time architecture creates unique debugging challenges. Audio glitches manifest as millisecond stutters requiring real-time profiling. Control typos cause silent failures—no compilation error, just features that don't work. Thread safety violations cause rare, hard-to-reproduce crashes. Understanding common failure modes and their diagnostic techniques saves hours of debugging time.

Common Issues and Solutions

"My control change didn't trigger!"

  • Verify control name matches exactly (case-sensitive, snake_case)
  • Check if bIgnoreNops=true and you're setting same value
  • Ensure Qt::ConnectionType is correct for your thread
  • Use qDebug() << m_pControl->get() to verify current value

"My audio is glitching!"

  • Remove any new, malloc, QString, or container resizing from audio thread
  • Replace mutexes with atomics (ControlProxy, QAtomicInt)
  • Check CPU usage during playback
  • Use DEBUG_ASSERT to catch violations early

"My DAO operation crashes!"

  • Only call DAO methods from library thread
  • Use Qt signals to queue database operations from other threads
  • Never access database from audio or GUI threads directly

"My widget doesn't update!"

  • Verify <Connection> in skin XML matches control ConfigKey
  • Check control is actually changing (use controller or dev tools)
  • Ensure widget's onConnectedControlChanged() is called
  • Try Qt::QueuedConnection if crossing threads

Debugging Techniques

Trace control changes (Control System, Observer Pattern):

connect(m_pControl, &ControlObject::valueChanged, [](double value) {
		qDebug() << "Control changed:" << value;  // *[Qt Integration Patterns](#qt-integration-patterns)*
});

Audio thread debugging (Real-Time Audio Thread Requirements, Thread Safety Considerations):

void process(...) {  // *[Audio Buffer Processing](#audio-buffer-processing)*
		DEBUG_ASSERT(!QApplication::instance()->thread() == QThread::currentThread());
		// Your code - assertion fires if called from wrong thread (*[Anti-Patterns](#anti-patterns)*)
}

Log to file (not in audio thread!) (Debugging Strategies):

qDebug() << "Feature triggered with value:" << value;  // Outputs to console/log
// WARNING: Never call from audio thread! (*[Anti-Patterns](#anti-patterns)*)

Debugging architecture reference:

History (Version History):

  • 1.0 (2002): Basic qDebug() logging
  • 1.6 (2008): --developer flag for verbose logging
  • 1.9 (2011): --controllerDebug flag for MIDI/HID message logging
  • 1.10 (2012): Valgrind memcheck integration for leak detection
  • 1.11 (2013): stats.log performance metrics file for audio thread profiling
  • 2.0 (2017): Thread sanitizer integration catches race conditions
  • 2.1 (2018): GDB pretty-printers for Mixxx types
  • 2.3 (2021): Structured logging with log levels (DEBUG, INFO, WARNING, ERROR) replaced raw qDebug()
  • 2.4 (2024): Crash reporting integration via Sentry

Testing Infrastructure

This section explains Mixxx's comprehensive testing framework built on GoogleTest (Build System, CMake Configuration), showing test base classes, audio engine testing patterns, mock objects, and controller script testing. Understanding the testing infrastructure is essential for writing robust tests and preventing regressions.

Why This Matters: Mixxx has 10,000+ lines of test code preventing regressions. The MixxxTest base class provides fixture setup (config, database, test directories, Configuration Files). Audio engine tests require deterministic buffer processing—no real-time threads (Audio Thread). Mock objects enable testing subsystems in isolation (Dependency Injection). Controller tests verify JavaScript behavior without hardware (Controllers). Writing tests is mandatory for pull requests—understand the patterns to write effective tests quickly (Code Review Best Practices).

Test Base Classes

MixxxTest - Base class for all tests:

class MixxxTest : public testing::Test {  // Google Test framework
	protected:
		// Setup/teardown (called before/after each test)
		void SetUp() override {
				// Creates temporary directories
				// Initializes UserSettings with test config
				// Sets up logging to test output
		}
		
		void TearDown() override {
				// Cleans up temporary files
				// Flushes any pending database writes
				// Verifies no ControlObject leaks
		}
		
		// Accessors:
		UserSettingsPointer config() const;  // Test-specific config (isolated)
		QDir getTestDataDir() const;         // Temporary dir: /tmp/mixxx-test-XXXXXX
		QString getTestDir() const;          // Base test directory
		
		// Utilities:
		TrackPointer newTestTrack() const;   // Create track with test audio
		QString makeTestConfigFile(const QString& path) const;
		
	private:
		UserSettingsPointer m_pConfig;       // Isolated config (doesn't touch ~/.mixxx)
		QTemporaryDir m_testDir;             // Auto-deleted on teardown
};

MixxxDbTest - For tests needing database:

class MixxxDbTest : public MixxxTest {
	protected:
		void SetUp() override {
				MixxxTest::SetUp();
				// Creates in-memory SQLite database (:memory:)
				// Runs schema migrations to latest version
				// Initializes all DAOs (TrackDAO, CueDAO, etc.)
		}
		
		void TearDown() override {
				// Closes database connections
				// Verifies no open transactions
				MixxxTest::TearDown();
		}
		
		// Database access:
		QSqlDatabase dbConnection() const;   // Test database connection
		TrackCollectionManager* collection() const;  // Track collection with DAOs
		
	private:
		std::unique_ptr<TrackCollectionManager> m_pTrackCollection;
};

BaseEngineTest - For audio engine tests:

class BaseEngineTest : public MixxxDbTest {
	protected:
		void SetUp() override {
				MixxxDbTest::SetUp();
				// Creates EngineMaster with test configuration
				// Sets up EngineBuffer in deterministic mode (no threads)
				// Configures test sample rate (44100 Hz)
				// Initializes EngineControls (CueControl, LoopingControl, etc.)
		}
		
		// Audio processing:
		void ProcessBuffer(int frames = 512);  // Process one buffer (deterministic)
		void Seek(double position);            // Seek to position
		void LoadTrack(TrackPointer pTrack);   // Load track into engine
		
		// Sample rate: 44100 Hz
		// Buffer size: 512 frames (configurable)
		// No real-time threads - fully deterministic
		
	protected:
		EngineMaster* m_pEngineMaster;         // Audio engine
		EngineBuffer* m_pEngineBuffer;         // Deck 1 buffer
		EngineChannel* m_pChannel1;            // Deck 1 channel
};

GoogleTest Framework

Mixxx uses GoogleTest (gtest) for unit testing:

Test macros:

// Test without fixture (simple standalone test):
TEST(SuiteName, TestName) {
		EXPECT_EQ(42, getValue());        // Non-fatal assertion (continues on failure)
		ASSERT_NE(nullptr, ptr);          // Fatal assertion (aborts test on failure)
}

// Test with fixture (uses test class for setup):
TEST_F(CueControlTest, HotcueSetClear) {
		// this-> accesses fixture members (m_pEngineBuffer, etc.)
		EXPECT_DOUBLE_EQ(1000.0, getPosition());  // Floating point equality (epsilon)
		ASSERT_TRUE(isLoopActive());              // Boolean check
}

// Parameterized test (runs same test with different inputs):
class BpmDetectionTest : public MixxxTest,
												 public testing::WithParamInterface<int> {
		// GetParam() returns current parameter
};

TEST_P(BpmDetectionTest, DetectsBpm) {
		int expectedBpm = GetParam();
		TrackPointer pTrack = loadTrackWithBpm(expectedBpm);
		EXPECT_NEAR(expectedBpm, pTrack->getBpm(), 0.1);  // Within 0.1 BPM
}

INSTANTIATE_TEST_SUITE_P(BpmRange,
												 BpmDetectionTest,
												 testing::Values(60, 90, 120, 128, 140, 174));
// Runs test 6 times with different BPM values

Assertion macros (comprehensive reference):

// Boolean:
EXPECT_TRUE(condition);
EXPECT_FALSE(condition);

// Comparison:
EXPECT_EQ(expected, actual);   // ==
EXPECT_NE(val1, val2);         // !=
EXPECT_LT(val1, val2);         // <
EXPECT_LE(val1, val2);         // <=
EXPECT_GT(val1, val2);         // >
EXPECT_GE(val1, val2);         // >=

// Floating point (with epsilon):
EXPECT_FLOAT_EQ(expected, actual);   // float equality
EXPECT_DOUBLE_EQ(expected, actual);  // double equality
EXPECT_NEAR(val1, val2, abs_error);  // |val1 - val2| <= abs_error

// String:
EXPECT_STREQ(str1, str2);      // C string equality
EXPECT_STRCASEEQ(str1, str2);  // Case-insensitive

// Exceptions:
EXPECT_THROW(statement, exception_type);
EXPECT_NO_THROW(statement);
EXPECT_ANY_THROW(statement);

// Pointers:
EXPECT_EQ(nullptr, ptr);       // Null check
EXPECT_NE(nullptr, ptr);       // Non-null check

// Death tests (verifies crashes/aborts):
EXPECT_DEATH(statement, regex);  // Expects crash with message matching regex

Accessing private members (use sparingly):

class CueControl {
		// In class declaration (header file):
		FRIEND_TEST(CueControlTest, HotcueSetClear);  // Grants access to private members
		
	private:
		void updateHotcues();  // Normally private, but test can call it
};

TEST_F(CueControlTest, HotcueSetClear) {
		// Can access private CueControl::updateHotcues()
		m_pCueControl->updateHotcues();
}
// Note: Prefer testing public API over private implementation

Test execution:

# Run all tests:
cd build && ctest
# Or:
./build/mixxx-test

# Run specific test:
./build/mixxx-test --gtest_filter=CueControlTest.HotcueSetClear

# Run all tests in a suite:
./build/mixxx-test --gtest_filter=CueControlTest.*

# List all tests:
./build/mixxx-test --gtest_list_tests

# Verbose output:
./build/mixxx-test --gtest_print_time=1

# Repeat test (find flaky tests):
./build/mixxx-test --gtest_repeat=100 --gtest_filter=ThreadSafetyTest.*

# Shuffle test order (find order dependencies):
./build/mixxx-test --gtest_shuffle

Testing reference: GoogleTest documentation at GoogleTest Primer and Mixxx testing guide at Unit Tests in Mixxx wiki. Browse test code at src/test/

Audio Engine Testing Patterns

AudioBufferTest provides buffer utilities for audio engine tests:

class EngineControlTest : public BaseEngineTest {
	protected:
		// BaseEngineTest provides EngineBuffer, EngineMaster setup
		void ProcessBuffer();  // Processes one audio buffer
		
		mixxx::AudioSourcePointer m_pAudioSource;
		EngineBufferPointer m_pEngineBuffer;
};

TEST_F(EngineControlTest, LoopActivation) {
		// Load test track
		TrackPointer pTrack = newTestTrack();
		m_pEngineBuffer->loadTrack(pTrack);
		
		// Set loop endpoints
		ControlObject::set(ConfigKey("[Channel1]", "loop_start_position"), 1000.0);
		ControlObject::set(ConfigKey("[Channel1]", "loop_end_position"), 2000.0);
		
		// Activate loop
		ControlObject::set(ConfigKey("[Channel1]", "loop_enabled"), 1.0);
		
		// Process buffers to verify looping
		ProcessBuffer();
		
		double position = ControlObject::get(ConfigKey("[Channel1]", "playposition"));
		EXPECT_GE(position, 1000.0);
		EXPECT_LE(position, 2000.0);
}

Testing audio thread safety:

// Verify process() doesn't allocate
TEST_F(EffectTest, ProcessDoesNotAllocate) {
		// Set up effect
		auto pEffect = createEffect();
		
		// Process with allocation detector
		AllocationChecker checker;
		pEffect->process(buffer, bufferSize);
		
		EXPECT_EQ(0, checker.getAllocationCount());
}

Mock Objects and Dependency Injection

Mock managers for isolated testing:

class MockPlayerManager : public PlayerManager {
	public:
		MOCK_METHOD(BaseTrackPlayer*, getPlayer, (const QString& group), (const, override));
		MOCK_METHOD(void, slotLoadTrackToPlayer, (TrackPointer pTrack, const QString& group));
};

TEST(ControllerTest, LoadTrackCommand) {
		MockPlayerManager mockPlayerManager;
		
		EXPECT_CALL(mockPlayerManager, slotLoadTrackToPlayer(_, "[Channel1]"))
				.Times(1);
		
		controller.handleMidiMessage(0x90, 0x01, 0x7F);  // Simulate MIDI input
}

Dependency injection in tests:

// Production code accepts interfaces
class EngineControl {
	public:
		EngineControl(UserSettingsPointer pConfig);  // Injectable
};

// Test provides mock config
TEST_F(EngineControlTest, ConfigBehavior) {
		auto mockConfig = std::make_shared<MockUserSettings>();
		EXPECT_CALL(*mockConfig, getValue(_, _))
				.WillOnce(Return(ConfigValue(42)));
		
		EngineControl control(mockConfig);
		// Test behavior with mocked config
}

Controller Testing

ControllerPresetTest validates controller mappings:

class ControllerPresetTest : public MixxxTest {
	protected:
		void loadPreset(const QString& filename);
		void sendMidiMessage(unsigned char status, unsigned char data1, unsigned char data2);
		
		std::unique_ptr<MidiController> m_pController;
};

TEST_F(ControllerPresetTest, PlayButtonMapping) {
		loadPreset("Novation-Launchpad-Pro-MK3.midi.xml");
		
		// Simulate button press
		sendMidiMessage(0x90, 0x10, 0x7F);  // Note On
		
		// Verify control changed
		double play = ControlObject::get(ConfigKey("[Channel1]", "play"));
		EXPECT_EQ(1.0, play);
}

Testing JavaScript controller scripts:

TEST_F(ControllerScriptTest, HotcueLabelScript) {
		loadScript("Launchpad-Pro-MK3-scripts.js");
		
		// Call JavaScript function from C++
		QJSValue result = evaluateScript(
				"MyController.setHotcueLabel(1, 1, 'Intro');"
		);
		
		EXPECT_FALSE(result.isError());
		
		// Verify control was set
		double position = ControlObject::get(
				ConfigKey("[Channel1]", "hotcue_1_position"));
		EXPECT_GT(position, 0.0);
}

Testing architecture reference:

History:

  • 2001-2006: Manual testing only
  • 1.9 (2011): GoogleTest adopted - automated unit testing begins
  • 1.10 (2012): MixxxTest base class with fixture setup (temp directories, database, config)
  • 1.11 (2013): EngineBufferTest for deterministic audio processing - caught sample-accurate bugs
  • 1.12 (2014): CI/CD integration running tests on every commit - dramatically reduced regressions
  • 2.0 (2017): Controller script testing via QJSEngine - prevented JavaScript regressions
  • 2.1 (2018): Mock objects for testing subsystems in isolation
  • 2.2 (2019): Benchmark tests for performance regression detection
  • 2.3 (2021): Code coverage reporting via codecov.io - revealed untested code paths

Common Troubleshooting Patterns

specific problems and solutions from real-world debugging.

symptom: audio glitches/clicks every few seconds

root cause: audio thread blocking
investigation:
	1. run with --developer --logLevel trace
	2. check logs for "XRUN" or "audio callback exceeded"
	3. profile with perf to find blocking function
	
common culprits:
	- memory allocation in audio thread (malloc/new)
	- mutex lock in audio callback
	- disk I/O in process() method
	- complex effect (reverb/echo with long buffer)
	
solution:
	- pre-allocate all buffers in constructor
	- use lock-free atomics instead of mutex
	- move disk I/O to separate thread
	- disable effects one by one to isolate

symptom: control not updating UI widget

root cause: missing signal connection or wrong thread
investigation:
	1. verify control exists: ControlObject::getControl(key)
	2. check widget setup() called by skin parser
	3. add qDebug() in onConnectedControlChanged()
	4. verify signal connected: qobject_cast<ControlProxy*>(sender())
	
common issues:
	- typo in ConfigKey ("[Channel1]", "play" vs "Play")
	- widget not added to skin XML <Connection>
	- signal/slot connection wrong thread type
	- control created after widget setup
	
solution:
	- use Qt::QueuedConnection for cross-thread signals
	- verify ConfigKey matches exactly
	- check skin XML has correct Group/Key

symptom: crash on track load

root cause: dangling pointer or uninitialized variable
investigation:
	1. run with valgrind: valgrind --leak-check=full ./mixxx
	2. check backtrace for crash location
	3. verify TrackPointer used (not raw Track*)
	4. check nullptr before dereferencing
	
common issues:
	- using Track* instead of TrackPointer
	- accessing m_pTrack after track unloaded
	- forgetting to check pTrack != nullptr
	- race condition (track unloaded while processing)
	
solution:
	- always use TrackPointer (QSharedPointer)
	- check !pTrack.isNull() before access
	- copy TrackPointer to local variable
	- never store raw Track* pointers

symptom: memory leak (RAM grows continuously)

root cause: forgotten delete or circular reference
investigation:
	1. valgrind --tool=massif ./mixxx
	2. ms_print massif.out | grep "peak memory"
	3. check for "still reachable" allocations
	4. instruments → Allocations (macOS)
	
common causes:
	- `new` without corresponding `delete`
	- QObject without parent (not in tree)
	- ControlProxy not deleted in destructor
	- circular QSharedPointer references
	
solution:
	- use RAII (std::unique_ptr, QObject parent)
	- set parent for all QObjects
	- break circular references with QWeakPointer
	- use Qt object tree for automatic cleanup

symptom: controller mapping not working

root cause: script error or MIDI mapping mismatch
investigation:
	1. check JavaScript console: Preferences → Controllers → Show Console
	2. verify MIDI messages received: enable MIDI logging
	3. add print() statements in script callbacks
	4. check controller sends expected MIDI values
	
common issues:
	- script syntax error (check console)
	- MIDI channel mismatch in XML (<channel> tag)
	- control/status byte wrong in mapping
	- soft-takeover preventing parameter change
	
solution:
	- fix script errors shown in console
	- verify MIDI messages with MIDI monitor
	- check XML <control>/<status> match hardware
	- disable soft-takeover for testing

symptom: database corruption "unable to open database file"

root cause: concurrent access or disk full
investigation:
	1. check disk space: df -h
	2. verify permissions: ls -la ~/.mixxx/mixxx.db
	3. close other Mixxx instances
	4. backup database before repair
	
recovery:
	1. cp ~/.mixxx/mixxx.db ~/.mixxx/mixxx.db.backup
	2. sqlite3 ~/.mixxx/mixxx.db
	3. PRAGMA integrity_check;
	4. .exit
	5. if corrupted: restore from backup or rescan
	
prevention:
	- only one Mixxx instance per database
	- don't store database on network drive
	- regular backups of ~/.mixxx/

symptom: waveform not rendering

root cause: OpenGL not available or driver issue
investigation:
	1. check logs for "OpenGL version" message
	2. Preferences → Waveforms → check backend
	3. try software renderer (Qt)
	4. update graphics drivers
	
solutions:
	- switch to software renderer if OpenGL fails
	- enable OpenGL in display settings (Linux)
	- update GPU drivers
	- try --disable-gl-widget flag

symptom: sync not working between decks

root cause: beat grid missing or BPM detection failed
investigation:
	1. check track has beat grid (waveform shows beats)
	2. analyze tracks if missing: Library → Analyze
	3. verify BPM in track properties
	4. check sync mode (follower/leader)
	
common issues:
	- track never analyzed (no beat grid)
	- beat detection failed (check BPM value)
	- wrong beat grid phase (manually adjust)
	- one deck not in sync mode
	
solution:
	- force re-analysis with correct BPM
	- manually adjust beat grid in editor
	- enable sync on both decks
	- check BPM values are reasonable (60-200)

Performance Optimization

This section provides detailed performance optimization strategies specific to Mixxx's architecture (Performance Characteristics, Real-Time Audio Thread Requirements), covering control access patterns, memory/cache optimization, profiling tools, and per-feature performance costs. These techniques are essential for maintaining real-time audio performance.

Why This Matters: The audio thread (Audio Thread) has a ~5-20ms budget—exceeding it causes audible glitches (Anti-Patterns). Even "fast" operations accumulate: reading a control via QString lookup takes 50x longer than cached ControlProxy (Control Access Patterns). Cache misses from poor memory layout cause mysterious slowdowns (Memory Management). Understanding per-feature costs (effect: 5-20% CPU, Effects System; complex control: 0.1% CPU, EngineControl) helps make informed architecture decisions. Profiling reveals bottlenecks (Profiling Tools)—never optimize without measuring first.

Control Access Patterns

Batch reads (Audio Thread, Real-Time Audio Thread Requirements):

// Good: Read once per buffer
double rate = m_pRate->get();  // One atomic load (~2ns)
for (int i = 0; i < bufferSize; i++) {
		// Use cached 'rate' value from register/L1 cache
		pOutput[i] = pInput[i] * rate;
}
// Cost: ~2ns + (bufferSize * ~1ns) = ~514ns for 512 samples

Avoid repeated atomic reads (Anti-Patterns):

// Bad: Atomic read every sample
for (int i = 0; i < bufferSize; i++) {
		double rate = m_pRate->get();  // 512 atomic loads!
		pOutput[i] = pInput[i] * rate;
}
// Cost: 512 * ~2ns = ~1024ns (2x slower)
// Plus: Prevents compiler vectorization (SIMD)

ControlProxy caching for frequently-read controls (Control System, ControlProxy):

class MyControl : public EngineControl {
	private:
		// GOOD: Member variable, initialized once in constructor
		ControlProxy* m_pRate;
		ControlProxy* m_pBpm;
		
	public:
		MyControl() {
			m_pRate = new ControlProxy("[Channel1]", "rate", this);
			m_pBpm = new ControlProxy("[Channel1]", "bpm", this);
			// One-time cost: 2 * ~500ns = ~1µs at construction
		}
		
		void process() {
			double rate = m_pRate->get();  // ~2ns (cached pointer)
			double bpm = m_pBpm->get();    // ~2ns
		}
};

// BAD: Lookup by string every callback
void process() {
	ControlObject* pRate = ControlObject::getControl(
			ConfigKey("[Channel1]", "rate"));  // ~50ns hash lookup!
	double rate = pRate->get();
}
// Cost: 50ns lookup + 2ns read = 25x slower than cached ControlProxy

Control access reference: Control architecture in Control System Details, addressing in ConfigKey, engine integration in EngineControl

Lock-Free Control Storage

atomic operations are the foundation of Mixxx's thread-safe control system (Control System):

ControlDoublePrivate implementation (src/control/controldoubleprivate.h):

class ControlDoublePrivate : public QObject {
	private:
		// lock-free storage using Qt atomics
		QAtomicInt m_value;  // stores double as reinterpreted bits
		
	public:
		// lock-free read (safe from any thread)
		double get() const {
				// atomic load with acquire semantics
				int bits = m_value.loadAcquire();
				double value;
				memcpy(&value, &bits, sizeof(double));
				return value;
		}
		
		// lock-free write (safe from any thread)
		void set(double newValue) {
				int newBits;
				memcpy(&newBits, &newValue, sizeof(double));
				
				// atomic exchange with release semantics
				int oldBits = m_value.fetchAndStoreRelease(newBits);
				
				// check if value actually changed
				if (oldBits != newBits) {
						// emit signal on GUI thread (queued connection)
						emit valueChanged(newValue);
				}
		}
};

memory ordering semantics (critical for correctness):

// acquire semantics: reads after loadAcquire() see writes before storeRelease()
// release semantics: writes before storeRelease() visible to loadAcquire()

// example: audio thread writes position, GUI thread reads it
// audio thread:
m_position.storeRelease(newPosition);  // ensure all prior writes visible

// GUI thread:
double pos = m_position.loadAcquire();  // see audio thread's writes

// without proper ordering:
// - GUI might see stale position (reordered reads)
// - audio thread writes might be reordered (wrong order)

memory ordering levels:

// relaxed: no ordering guarantees (fastest, ~1ns)
m_counter.fetchAndAddRelaxed(1);  // ok for simple counters

// acquire/release: synchronizes with matching release/acquire (~2ns)
double value = m_control.loadAcquire();  // use for control reads

// sequential consistency: global ordering, slowest (~5ns)
// mixxx doesn't use this (too slow for audio thread)

std::atomic vs QAtomicInteger:

// modern Mixxx code (2.6.0+): prefer std::atomic
std::atomic<double> m_value{0.0};

double get() const {
		return m_value.load(std::memory_order_acquire);
}

void set(double newValue) {
		m_value.store(newValue, std::memory_order_release);
}

// legacy code: QAtomicInt (Qt 5 compatibility)
// still used in ControlDoublePrivate for Qt signals integration

lock-free ring buffer for audio I/O (src/engine/cachingreader.cpp):

class RingBuffer {
	private:
		std::atomic<int> m_writePos{0};   // producer updates
		std::atomic<int> m_readPos{0};    // consumer updates
		CSAMPLE* m_buffer;                // pre-allocated buffer
		int m_capacity;
		
	public:
		// called from disk I/O thread (producer)
		void write(const CSAMPLE* data, int count) {
				int writePos = m_writePos.load(std::memory_order_relaxed);
				int readPos = m_readPos.load(std::memory_order_acquire);
				
				int available = (readPos - writePos - 1 + m_capacity) % m_capacity;
				if (count > available) {
						count = available;  // buffer full, drop samples
				}
				
				// copy data (no lock needed, single producer)
				memcpy(&m_buffer[writePos], data, count * sizeof(CSAMPLE));
				
				// publish write (release ensures writes visible to consumer)
				int newWritePos = (writePos + count) % m_capacity;
				m_writePos.store(newWritePos, std::memory_order_release);
		}
		
		// called from audio thread (consumer)
		int read(CSAMPLE* dest, int count) {
				int readPos = m_readPos.load(std::memory_order_relaxed);
				int writePos = m_writePos.load(std::memory_order_acquire);
				
				int available = (writePos - readPos + m_capacity) % m_capacity;
				if (count > available) {
						count = available;  // underrun, return silence
				}
				
				// copy data (no lock needed, single consumer)
				memcpy(dest, &m_buffer[readPos], count * sizeof(CSAMPLE));
				
				// publish read (release ensures reads visible to producer)
				int newReadPos = (readPos + count) % m_capacity;
				m_readPos.store(newReadPos, std::memory_order_release);
				
				return count;
		}
};

when to use lock-free patterns: - ✓ control reads/writes: always (ControlProxy uses atomics) - ✓ simple counters: atomic increment/decrement - ✓ ring buffers: single-producer single-consumer (SPSC) - ✗ complex data structures: use pre-allocated data + atomics for coordination - ✗ multiple producers/consumers: lock-free queues are complex, usually not worth it

Parameter vs value space:

// Parameter space (0.0-1.0, normalized)
double param = m_pVolume->getParameter();  // Slightly slower (~5ns)
// Involves ControlBehavior transformation

// Value space (native range, e.g., dB)
double value = m_pVolume->get();  // Faster (~2ns)
// Direct atomic read, no transformation

// Rule: Use get() in audio thread, getParameter() for UI/controllers

Write patterns (minimize signal emission overhead):

// Position updates (every callback):
m_pPlayposition->set(newPosition);  // ~5ns
// Note: Uses bIgnoreNops=true to skip redundant valueChanged() signals

// Control that changes frequently:
m_pVuMeter->set(level);  // Set bIgnoreNops=false if every value matters
// Each set() that changes value: ~5ns + ~100ns per connected slot

Memory and Cache Optimization

Cache-friendly data layout (critical for performance):

// GOOD: Sequential access (cache-friendly)
std::vector<CSAMPLE> buffer(bufferSize);  // Contiguous memory
for (std::size_t i = 0; i < bufferSize; ++i) {
		buffer[i] = process(buffer[i]);  // Predictable access pattern
}
// Modern CPU prefetcher loads next cache line speculatively
// ~64 bytes per cache line = 16 floats (CSAMPLE = float)
// Result: ~95% cache hit rate, ~1ns per access

// BAD: Random access (cache-unfriendly)
std::map<int, CSAMPLE> buffer;  // Scattered in memory (red-black tree)
for (auto& pair : buffer) {
		pair.second = process(pair.second);  // Random memory access
}
// Each access may miss L1/L2 cache: ~10-100ns per access
// Result: 10-100x slower than sequential access

Struct layout optimization (arrange by access pattern):

// GOOD: Hot data together
struct EngineState {
		// Frequently accessed (every callback):
		double currentPosition;     // Read/write every callback
		double rate;                // Read every callback
		bool playing;               // Read every callback
		// Total: 24 bytes (fits in single cache line)
		
		// Padding to next cache line (avoid false sharing)
		char padding[40];
		
		// Cold data (accessed rarely):
		QString trackPath;          // Only on track load
		QDateTime loadTime;         // Only on track load
};
// Hot data in first cache line, cold data doesn't pollute cache

// BAD: Interleaved hot/cold
struct EngineState {
		double currentPosition;     // Hot
		QString trackPath;          // Cold (but forces cache line load)
		double rate;                // Hot (in different cache line!)
		QDateTime loadTime;         // Cold
		bool playing;               // Hot (yet another cache line!)
};
// Hot data scattered across 3 cache lines = 3x cache misses

Avoid allocations in hot paths:

// GOOD: Pre-allocate (zero allocations in process())
class MyProcessor {
	private:
		std::vector<CSAMPLE> m_buffer;  // Member variable
		
	public:
		MyProcessor() {
				m_buffer.reserve(MAX_BUFFER_SIZE);  // One-time allocation
		}
		
		void process(int bufferSize) {
				m_buffer.resize(bufferSize);  // No allocation if <= capacity
				// ... process m_buffer ...
		}
};
// Cost: 0 allocations in process()

// BAD: Allocate every call
void process(int bufferSize) {
		std::vector<CSAMPLE> buffer(bufferSize);  // malloc() every callback!
		// ... process buffer ...
}
// Cost: 1-10ms per allocation (catastrophic for audio thread)

SIMD optimization (enable compiler auto-vectorization):

// GOOD: Vectorizable loop (compiler generates SSE/AVX)
for (SINT i = 0; i < bufferSize; i++) {  // SINT = ptrdiff_t (signed)
		pOutput[i] = pInput[i] * gain;  // Simple, predictable operation
}
// Compiler generates: movaps, mulps (processes 4-8 floats at once)
// Speedup: 4x on SSE, 8x on AVX

// BAD: Prevents vectorization
for (int i = 0; i < bufferSize; i++) {
		if (pInput[i] > threshold) {  // Conditional breaks vectorization
				pOutput[i] = complexFunction(pInput[i]);  // Function call prevents inline
		}
}
// Compiler falls back to scalar code (1 float at a time)

Memory alignment (required for SIMD):

// GOOD: Aligned allocation
alignedArray<CSAMPLE, 16> buffer(bufferSize);  // 16-byte aligned
// SSE/AVX instructions require aligned memory
// Unaligned access: +10-20% performance penalty

// Check alignment:
assert(reinterpret_cast<uintptr_t>(buffer.data()) % 16 == 0);

False sharing prevention (multi-threaded):

// BAD: False sharing between threads
struct CounterPair {
		std::atomic<int> threadA;  // Offset 0
		std::atomic<int> threadB;  // Offset 4 (same cache line!)
};
// Thread A writes threadA → invalidates Thread B's cache line
// Thread B writes threadB → invalidates Thread A's cache line
// Result: Cache line ping-pong, 10-100x slower

// GOOD: Separate cache lines
struct alignas(64) Counter {
		std::atomic<int> value;
		char padding[60];  // Pad to 64 bytes (typical cache line size)
};
Counter threadA;  // Own cache line
Counter threadB;  // Own cache line
// No false sharing, each thread has exclusive cache line

Profiling

Rule #1: Never optimize without measuring first. Profile to find bottlenecks, optimize, then profile again to verify improvement.

Linux (perf) - CPU profiling with call graphs:

# Record with call graph (captures stack traces)
perf record -g --call-graph dwarf -F 999 ./mixxx
# -g: Enable call graph
# --call-graph dwarf: Use DWARF debug info (more accurate than frame pointers)
# -F 999: Sample at 999 Hz (prime number reduces sampling bias)

# View results interactively
perf report
# Navigate with arrow keys, press 'a' to annotate assembly

# Generate flamegraph (visual representation)
perf script | ./flamegraph.pl > mixxx.svg
# Download: https://github.com/brendangregg/FlameGraph

# Focus on specific function:
perf report --stdio --dsos=mixxx | grep EngineBuffer

# Record cache misses:
perf record -e cache-misses -g ./mixxx
perf report --sort=dso,symbol

# Record branch mispredictions:
perf record -e branch-misses -g ./mixxx

macOS (Instruments) - Xcode profiling tools:

# Time Profiler - CPU profiling
# 1. Open Instruments.app
# 2. Choose "Time Profiler" template
# 3. Select Mixxx.app
# 4. Record during problematic operation
# 5. View Call Tree, filter by thread name

# Allocations - Memory profiling
# 1. Choose "Allocations" template
# 2. Record, look for spikes during audio callback
# 3. Filter to "AudioThread" to find allocations in audio thread
# 4. Any allocation in audio thread is a bug!

# System Trace - Thread scheduling
# 1. Choose "System Trace"
# 2. View thread activity, context switches
# 3. Look for audio thread preemption (bad!)

Windows (Visual Studio Profiler):

# Performance Profiler
# 1. Debug > Performance Profiler
# 2. Select "CPU Usage" and "Memory Usage"
# 3. Start profiling
# 4. View Hot Path, Flame Graph

# Or use VTune Profiler (Intel)

Valgrind (Callgrind) - Instruction-level profiling:

# Profile CPU usage (very slow, ~20x slowdown)
valgrind --tool=callgrind --separate-threads=yes ./mixxx
# Generates callgrind.out.<pid>

# Visualize with KCachegrind:
kcachegrind callgrind.out.12345
# Shows call graph, source annotations, instruction counts

# Profile cache misses:
valgrind --tool=cachegrind ./mixxx
# Shows L1/LL cache miss rates per function

Valgrind (Helgrind) - Thread race detector:

valgrind --tool=helgrind ./mixxx
# Detects data races, lock ordering issues
# Example output: "Possible data race during write at 0x12345678"

AddressSanitizer (ASan) - Memory error detector:

# Compile with sanitizer:
cmake -DCMAKE_CXX_FLAGS="-fsanitize=address -g" ..
make

# Run (crashes on first error):
./mixxx
# Detects: use-after-free, buffer overflow, memory leaks
# Audio thread allocations show up as leaks (reported at exit)

ThreadSanitizer (TSan) - Data race detector:

# Compile with TSan:
cmake -DCMAKE_CXX_FLAGS="-fsanitize=thread -g" ..
make

./mixxx
# Detects: data races, lock order inversions
# Example: WARNING: ThreadSanitizer: data race on ControlDoublePrivate::m_value

Use Mixxx's built-in stats (control access profiling):

// Enable statistics tracking for control:
m_pControl = new ControlObject(key, 
																false,  // bIgnoreNops
																true);  // bTrack=true enables stats

// View stats in Developer > Stats menu (Mixxx UI)
// Shows: get() count, set() count, last value, min/max
// Useful for finding controls that are read/written too frequently

Manual timing (microbenchmarking):

#include <QElapsedTimer>

void MyControl::process(const double rate,
												mixxx::audio::FramePos currentPosition,
												const std::size_t bufferSize) {
		QElapsedTimer timer;
		timer.start();
		
		// ... processing code ...
		
		qint64 elapsed = timer.nsecsElapsed();
		if (elapsed > 100000) {  // 100µs threshold
				qWarning() << "MyControl::process() took" << elapsed / 1000 << "µs"
									 << "for buffer size" << bufferSize;
		}
		
		// Or use performance counter:
		static qint64 totalNanos = 0;
		static int callCount = 0;
		totalNanos += elapsed;
		callCount++;
		if (callCount % 1000 == 0) {
				qDebug() << "Average:" << (totalNanos / callCount / 1000) << "µs per call";
		}
}

Profiling strategies:

  1. Top-down: Profile entire app, identify hot functions, drill down
  2. Bottom-up: Microbenchmark critical function, optimize, integrate
  3. Differential: Profile before/after optimization to verify improvement
  4. Load testing: Profile with 4 decks + 16 effects vs. 2 decks baseline

Performance regression detection:

# Benchmark script (run before/after changes):
#!/bin/bash
# Load 4 tracks, play for 60 seconds, measure CPU%
perf stat -e cycles,instructions,cache-misses,branch-misses \
	timeout 60s ./mixxx --settingsPath=/tmp/test-settings &
# Compare metrics between runs

Profiling reference:

Performance Profiling Techniques

profiling is mandatory before optimization—never optimize without measuring.

linux profiling (perf):

# record CPU profile with call stacks
perf record -g --call-graph=dwarf ./mixxx

# view flamegraph (where CPU time is spent)
perf report

# focus on audio thread (find PID first)
ps aux | grep mixxx
perf record -p <PID> -t <AUDIO_THREAD_TID> sleep 10

# annotate specific function with assembly
perf annotate EngineBuffer::process

interpreting perf results:

# example perf report output:
	42.31%  mixxx  [.] EngineBuffer::process
	18.23%  mixxx  [.] EffectChainMixerImpl::process
	12.15%  mixxx  [.] WaveformRendererRGB::draw
	 8.44%  mixxx  [.] EngineFilterBlock::process

# means:
# - EngineBuffer takes 42% of CPU
# - Effects take 18% (consider disabling complex effects)
# - Waveform rendering takes 12% (use simpler waveform type)
# - EQ takes 8% (expected)

macos profiling (instruments):

# launch with time profiler
instruments -t "Time Profiler" -l 30000 ./mixxx

# or from Xcode:
# Product → Profile → Time Profiler
# - check "High Frequency" for audio thread detail
# - filter to "mixxx" process
# - expand audio thread (usually thread 2 or 3)

windows profiling (VTune / Visual Studio):

# Visual Studio performance profiler:
# Debug → Performance Profiler → CPU Usage
# - run for 30 seconds
# - filter to audio thread
# - sort by "Self Time" (excludes callees)

# VTune (Intel):
vtune -collect hotspots -app-working-dir . -- ./mixxx.exe
vtune-gui result.amplxe

built-in mixxx stat tracking:

// enable via command line
./mixxx --developer --logLevel trace

// stats logged every second:
// [EngineMaster] Callback time: 3.2ms / 10.0ms (32% of budget)
// [EngineBuffer] Process time: 1.4ms (44% of callback)
// [EffectChain] Process time: 0.8ms (25% of callback)

manual timing (for specific sections):

#include "util/performancetimer.h"

void EngineBuffer::process(...) {
		PerformanceTimer timer;
		timer.start();
		
		// code to measure
		m_pScale->process(...);
		
		qint64 elapsed = timer.elapsed();  // nanoseconds
		qDebug() << "Scale took" << (elapsed / 1000.0) << "µs";
		
		// expected: < 500µs for 512 samples
}

audio thread xrun detection:

// monitor buffer underruns
void SoundManager::pushBuffer(...) {
		if (m_pMaster->getLatency() > m_bufferSize * 2) {
				qWarning() << "XRUN: Audio thread missed deadline!";
				// investigation:
				// 1. check perf report for hot spots
				// 2. verify no allocations in audio callback
				// 3. check for mutex locks
				// 4. disable effects one by one to isolate
		}
}

cache miss profiling:

# linux: count cache misses
perf stat -e cache-references,cache-misses ./mixxx

# expect:
# - cache references: ~10M/sec
# - cache misses: <5% (good), >10% (investigate)

# drill down to function
perf record -e cache-misses -g ./mixxx
perf report

memory profiling:

# linux: valgrind massif (heap profiler)
valgrind --tool=massif --massif-out-file=massif.out ./mixxx
ms_print massif.out

# shows:
# - peak memory usage
# - allocation call stacks
# - memory usage over time

# macos: instruments → Allocations
# - filter to "mixxx" process
# - check "Mark Heap" for leaked objects
# - "Persistent" bytes should be stable (not growing)

profiling workflow:

  1. establish baseline: measure before any changes
  2. isolate: disable features to find bottleneck
  3. profile: use perf/instruments on suspected code
  4. optimize: change one thing at a time
  5. verify: measure again, confirm improvement
  6. regression test: ensure no audio glitches introduced

red flags in profiles:

  • mutex contention: pthread_mutex_lock in top 10 functions
  • allocations: malloc, operator new in audio thread
  • string operations: QString constructors in hot path
  • cache misses: >10% miss rate on audio thread
  • system calls: read, write, ioctl in audio callback

Per-Feature Performance Costs

Understanding CPU/memory costs helps optimize Mixxx for different hardware (Performance Characteristics, Scaling Characteristics).

Related performance metrics: Memory usage in Memory Footprint, audio constraints in Real-Time Audio, analysis speed in Analysis Performance

Deck Features (per active deck):

  • Basic playback: ~2-5% CPU (1 core)
  • Key lock (pitch shift): +3-5% CPU
  • BPM sync active: +1-2% CPU
  • Loop active: +0.5% CPU (minimal)
  • Hotcue access: Negligible (<0.1%)

Effects (per effect in chain):

  • Simple (tremolo, autopan): +1-2% CPU
  • Medium (filter, EQ): +2-4% CPU
  • Complex (reverb, echo with long delay): +5-10% CPU
  • LV2 plugins: Varies widely (1-20% CPU depending on plugin)

Waveform Rendering (per waveform widget):

  • RGB waveform (OpenGL): +5-8% CPU, 50-100MB VRAM
  • RGB waveform (software): +15-20% CPU
  • Simple waveform: +2-5% CPU
  • Overview waveform: +1-2% CPU

Stems Processing:

  • Stereo playback: Baseline
  • 4-stem playback: +10-15% CPU (decoding 4 streams)
  • Per-stem effects: Multiply effect cost by number of stems

Track Analysis:

  • BPM detection: ~0.3-1x realtime (faster than playback)
  • Key detection: ~0.5-2x realtime
  • Waveform generation: ~2-5x realtime
  • ReplayGain: ~3-8x realtime
  • Parallel analysis (4 tracks): ~2-4x realtime per track on quad-core

Library Operations:

  • Track scanner: ~10-50 tracks/second (depends on disk speed)
  • Database query (sorted, filtered): <10ms for libraries <50k tracks
  • Track load (from cache): ~5-20ms
  • Track load (first time, no analysis): ~50-200ms

Recording/Broadcasting:

  • WAV recording: +1-2% CPU (minimal encoding)
  • MP3 encoding (192kbps): +3-5% CPU
  • OGG encoding (quality 6): +4-7% CPU
  • Broadcasting (network I/O): +1-2% CPU

Vinyl Control (DVS):

  • Timecode processing: +2-4% CPU per deck
  • Signal quality analysis: +1% CPU

Memory usage baselines:

  • Mixxx core: ~100-200MB
  • Per loaded track: ~2-10MB (waveform + metadata cache)
  • Library (10k tracks): ~50-100MB
  • Effects rack: ~10-30MB
  • Skin textures/UI: ~50-150MB

Optimization targets:

  • Low-end (2-core, integrated GPU): 2 decks, simple waveforms, minimal effects
  • Mid-range (4-core, mid GPU): 4 decks, RGB waveforms, multiple effect chains
  • High-end (8-core, dedicated GPU): 4 decks + samplers, stems, complex effects, recording

Bottlenecks by hardware:

Related performance topics:

History:

  • 2001-2006: No performance instrumentation
  • 1.9 (2011): ControlProxy caching patterns formalized after QString lookups caused audio dropouts
  • 1.10+ (2012): SSE/AVX SIMD optimizations for audio processing added progressively, GPU-accelerated waveforms reduced CPU from 30% to 5%
  • 1.11 (2013): stats.log performance metrics file enables audio thread profiling
  • 2.0 (2017): Memory pool allocators for frequently-allocated objects (buffers, samples), SoundTouch→RubberBand reduced time-stretching CPU by 40%
  • 2.1 (2018): Cache-line alignment for hot path structures
  • 2.2 (2019): Lock-free atomics for ControlObject replaced mutexes - eliminated priority inversion glitches

Migration and Version Notes

This section documents deprecated patterns and modern replacements, helping developers migrate legacy code and adopt current best practices. Understanding these transitions prevents using outdated patterns in new code and explains why older code looks different.

Why This Matters: Mixxx's codebase spans 15+ years—patterns evolved as C++ standards matured. DISALLOW_COPY_AND_ASSIGN was necessary before C++11's = delete. ControlObjectThreadMain predates ControlProxy's thread-safe design. Q_FOREACH has performance issues vs. range-based loops. Understanding deprecated patterns helps when reading old code, and knowing modern replacements ensures new code follows current standards.

Deprecated Patterns (Avoid in New Code)

Deprecated Modern Replacement Since
DISALLOW_COPY_AND_ASSIGN(Class) Class(const Class&) = delete; 2.3+
Raw QObject* without parent parented_ptr<QObject> 2.4+
ControlObjectThreadMain ControlProxy 2.0+
Q_FOREACH Range-based for loop 2.3+
NULL nullptr 2.2+

Modern C++ Features (Use These)

Smart Pointers (prefer over raw new/delete):

// Unique ownership
auto decoder = std::make_unique<Decoder>();
m_decoders.push_back(std::move(decoder));

// Shared ownership
auto track = QSharedPointer<Track>::create();
TrackPointer pTrack = Track::newTemporary();  // typedef for QSharedPointer

Range-based for loops:

// Clean iteration
for (const auto& track : trackList) {
		processTrack(track);
}

// With structured bindings (C++17)
for (const auto& [key, control] : m_controlMap) {
		control->reset();
}

Auto keyword (use judiciously):

// Good: Type is obvious from context
auto buffer = std::make_unique<CSAMPLE[]>(size);
auto it = map.find(key);

// Avoid: Type unclear
auto value = calculateSomething();  // What type is this?

Lambda expressions:

// Callbacks and inline functions
connect(pControl, &ControlObject::valueChanged,
				this, [this](double value) {
		if (value > 0.0) {
				handleActivation();
		}
});

// Algorithm usage
std::sort(tracks.begin(), tracks.end(),
					[](const Track& a, const Track& b) {
		return a.getBpm() < b.getBpm();
});

Move semantics:

// Efficient transfer of ownership
std::unique_ptr<Data> data = createData();
processData(std::move(data));  // Transfer ownership

// RVO/NRVO usually handles return values automatically
std::vector<Track> loadTracks() {
		std::vector<Track> tracks;
		// ... populate tracks
		return tracks;  // No explicit move needed
}

constexpr and const:

// Compile-time constants
constexpr int kMaxChannels = 32;
constexpr double kDefaultGain = 1.0;

// Runtime constants
const QString kGroupPrefix = QStringLiteral("[Channel");

// constexpr functions (C++14+)
constexpr int calculateBufferSize(int frames) {
		return frames * kMaxChannels;
}

std::array (prefer over C arrays):

// Type-safe, bounds-checked (in debug), STL-compatible
std::array<ControlProxy*, kMaxSupportedStems> m_stemGain;
std::array<CSAMPLE, kBufferSize> buffer{};  // Zero-initialized

C++17/20 features (available since Mixxx 2.3):

// Structured bindings
auto [key, value] = map.find(item);
for (const auto& [hotcueIndex, cue] : hotcues) {
		updateCue(hotcueIndex, cue);
}

// if-init statements (reduce scope)
if (auto it = map.find(key); it != map.end()) {
		return it->second;
}

// std::optional for nullable returns
std::optional<double> getBpm() {
		if (m_bpmLocked) {
				return m_bpm;
		}
		return std::nullopt;
}

// Fold expressions (variadic templates)
template<typename... Args>
void log(Args&&... args) {
		(std::cout << ... << args) << '\n';
}

// Class template argument deduction (CTAD)
std::array arr{1, 2, 3};  // Deduces std::array<int, 3>
std::pair p{42, "text"};  // Deduces types

std::string_view (C++17):

// Efficient string passing (no copies)
void processName(std::string_view name) {
		// Works with QString via toStdString()
		if (name.starts_with("Channel")) {
				// ...
		}
}

Designated initializers (C++20):

struct AudioConfig {
		int sampleRate;
		int bufferSize;
		int channels;
};

AudioConfig config{
		.sampleRate = 48000,
		.bufferSize = 1024,
		.channels = 2
};

Type traits and concepts (advanced):

// SFINAE with enable_if
template<typename T>
std::enable_if_t<std::is_arithmetic_v<T>, T>
clamp(T value, T min, T max) {
		return std::clamp(value, min, max);
}

// C++20 concepts (if available)
template<typename T>
concept Numeric = std::is_arithmetic_v<T>;

template<Numeric T>
T add(T a, T b) { return a + b; }

Attribute specifiers:

// Compiler hints
[[nodiscard]] bool loadTrack(TrackPointer track);  // Warn if return ignored
[[maybe_unused]] int debugValue = 42;              // Suppress unused warnings
[[fallthrough]];                                    // Intentional switch fallthrough

Qt 5 → Qt 6 migration:

  • QStringRefQStringView
  • QLinkedListstd::list or QList
  • New signal/slot syntax (function pointers) preferred over SIGNAL/SLOT macros
  • QVariant constructor changes (more type-safe)

Migration reference: Coding conventions in Coding Style Essentials and ownership patterns in Memory Management

History:

  • 1.9 (2011): TrackPointer (QSharedPointer) introduced - pioneered smart pointer migration
  • 1.12 (2014-2015): Qt 4→Qt 5 migration - extensive QRegExp→QRegularExpression refactoring
  • 2011-2019: ControlObjectThreadMain→ControlProxy migration as threading patterns evolved
  • 2.0 (2017): C++11 features adopted (auto, nullptr, range-for, smart pointers) after dropping ancient compiler support
  • C++14: DISALLOW_COPY_AND_ASSIGN macro replaced with = delete
  • 2.4 (2024): C++17 features began appearing (structured bindings, std::optional, inline variables)
  • 2015-2024: Gradual removal of raw pointers in favor of smart pointers throughout codebase
  • 2.5.0 (December 2024): Qt 5→Qt 6 migration completed - QStringRef→QStringView, signal/slot syntax updates
  • All migrations maintain backward compatibility for user data (settings, library) while modernizing code
  • Note: Version 2.3.x was never released; development jumped from 2.2.x to 2.4.0

Anti-Patterns

This section catalogs critical mistakes and best practices in Mixxx development, explaining anti-patterns that cause crashes, glitches, or maintenance nightmares, alongside recommended solutions. This is the "don't do this" companion to the architectural patterns throughout the guide (Key Architectural Patterns, Coding Style Essentials).

Why This Matters: These aren't theoretical concerns—every anti-pattern listed has caused real bugs in Mixxx. Audio thread allocations caused the "2.0 glitch bug" affecting thousands of users. Missing disconnect() calls caused controller hot-swap crashes. Qt::DirectConnection across threads caused rare race conditions. Understanding these anti-patterns prevents repeating history and helps code reviewers catch violations (Testing Infrastructure, Code Review Best Practices).

Critical Don'ts (Causes Crashes/Glitches)

❌ Allocate memory in audio callbacks (Real-Time Audio Thread Requirements)

void process(...) {
		auto buffer = new float[size];  // NEVER! Causes audio dropouts
		QString str = "test";  // NEVER! QString allocates
}

Why: Memory allocation can block for milliseconds, causing audio glitches. The audio thread must complete within 5-20ms (Performance Characteristics).
Fix: Pre-allocate in constructor (Memory Management), use stack buffers, or use lock-free structures (Lock-Free Programming).

❌ Use mutexes in audio thread (Audio Thread, Thread Safety Considerations)

void process(...) {
		QMutexLocker lock(&m_mutex);  // NEVER! Causes priority inversion
}

Why: Mutexes can block indefinitely if another thread holds the lock (Real-Time Audio Thread Requirements).
Fix: Use ControlProxy (atomic, Control System) or QAtomicInt for shared state (Control Access Patterns).

❌ Access Track directly from audio thread (Track Pointer Pattern, Thread Separation)

void process(...) {
		double bpm = m_pTrack->getBpm();  // NEVER! Internal locks
}

Why: Track objects use mutexes internally for thread safety (Library).
Fix: Cache track data in controls during trackLoaded() callback (see Example 3, Audio Buffer Processing).

Common Mistakes

❌ Wrong Qt connection type (Signal/Slot Connection Patterns, Qt Integration Patterns)

connect(control, &ControlObject::valueChanged,
				this, &MyClass::slot,
				Qt::DirectConnection);  // Wrong if 'this' is on GUI thread!

Why: DirectConnection calls slot in sender's thread. If sender is audio thread and receiver is GUI, you're calling GUI code from audio thread (Threading Model).
Fix: Use Qt::AutoConnection (default) or explicitly Qt::QueuedConnection for cross-thread (Three Fundamental Threads).

❌ Forget Q_OBJECT macro (Qt Meta-Object System, Build System)

class MyControl : public QObject {
		// Missing Q_OBJECT!
signals:
		void valueChanged();  // Won't work without Q_OBJECT
};

Why: Qt's meta-object compiler (MOC) needs Q_OBJECT to generate signal/slot machinery (CMake Configuration).
Fix: Add Q_OBJECT as first line in class body, ensure CMake AUTOMOC is enabled (Qt Integration).

❌ Database access from wrong thread (Library Thread, DAO Pattern)

void MyEngineControl::process(...) {
		m_pTrackDAO->saveTrack(track);  // NEVER! SQL from audio thread
}

Why: Database operations are blocking I/O, not allowed in audio thread (Real-Time Audio Thread Requirements).
Fix: Emit signal to library thread, perform operation there (Thread Safety Considerations, Library and Database Architecture).

❌ Deprecated patterns

class MyClass {
		DISALLOW_COPY_AND_ASSIGN(MyClass);  // Deprecated macro
};

Fix: Use modern C++11 syntax:

class MyClass {
		MyClass(const MyClass&) = delete;
		MyClass& operator=(const MyClass&) = delete;
};

Best Practices

  • ✅ Use ControlProxy for audio thread access (atomic, lock-free)
  • ✅ Use parented_ptr for Qt tree-managed objects (prevents leaks)
  • ✅ Follow snake_case for new ConfigKey items (consistency)
  • ✅ Write unit tests for new features (Google Test)
  • ✅ Use DEBUG_ASSERT liberally (catches bugs early in development)
  • ✅ Document thread safety assumptions in comments
  • ✅ Follow the Rule of Zero (let compiler generate special members)

Best practices reference: Debugging techniques in Debugging and Troubleshooting and optimization strategies in Performance Optimization

Signal/Slot Connection Pitfalls

Wrong connection type causes deadlocks:

// ❌ NEVER: Qt::BlockingQueuedConnection from audio → GUI thread
connect(audioThreadObject, &AudioObject::signal,
				guiObject, &GuiObject::slot,
				Qt::BlockingQueuedConnection);  // DEADLOCK RISK!

Why: Audio thread blocks waiting for GUI, but GUI may be blocked elsewhere.
Fix: Use Qt::QueuedConnection or Qt::AutoConnection.

DirectConnection across threads without thread-safety:

// ❌ NEVER: DirectConnection calling non-thread-safe code
connect(controlObject, &ControlObject::valueChanged,
				this, &MyClass::unsafeSlot,
				Qt::DirectConnection);  // Called from audio thread!

void MyClass::unsafeSlot(double value) {
		m_trackList.append(value);  // QList modification not thread-safe!
}

Fix: Use Qt::QueuedConnection for cross-thread signals to non-thread-safe slots.

Lambda capture causing use-after-free:

// ❌ DANGEROUS: Lambda outlives captured object
{
		MyObject obj;
		connect(control, &ControlObject::valueChanged,
						[&obj](double value) {  // Captures by reference!
				obj.setValue(value);  // obj destroyed after scope
		});
}  // obj destroyed, lambda still connected

Fix: Capture by value, use QPointer, or disconnect before destruction.

Forgetting to disconnect:

// ❌ Memory leak: Connection never removed
MyClass::MyClass() {
		connect(globalObject, &GlobalObject::signal,
						this, &MyClass::slot);
		// No corresponding disconnect in destructor
}

Fix: Use Qt::UniqueConnection flag, or store QMetaObject::Connection and disconnect in destructor.

Signal emission during destruction:

// ❌ Emitting signals in destructor
MyClass::~MyClass() {
		emit somethingChanged();  // Connected slots may access deleted members
}

Fix: Emit signals before destruction begins, or disconnect all slots first.

Connecting to deleted object:

// ❌ Connection to object without checking lifetime
void MyClass::connectToSomething(QObject* other) {
		connect(this, &MyClass::signal,
						other, &OtherClass::slot);  // What if 'other' is deleted?
}

Fix: Use Qt::UniqueConnection, connect with context object, or use QPointer<OtherClass>.

Recursive signal loops:

// ❌ Signal causes another signal causing first signal...
connect(controlA, &ControlObject::valueChanged,
				this, [this](double value) {
		controlB->set(value);  // Triggers controlB signal
});

connect(controlB, &ControlObject::valueChanged,
				this, [this](double value) {
		controlA->set(value);  // Triggers controlA signal → infinite loop!
});

Fix: Use guard flags, QSignalBlocker, or rethink control flow to avoid cycles.

Anti-patterns reference: Qt patterns in Qt Integration Patterns, control patterns in Value Change Request Pattern, threading in Thread Safety Considerations

History:

  • 2.9 (2011): ControlProxy caching pattern emerged after profiling revealed control lookups were bottleneck
  • 1.10 (2012): Valgrind integration exposed numerous lifetime issues - memory leak patterns cataloged
  • 2.0 (2017): "No allocations in audio thread" rule formalized after notorious "2.0 glitch bug" - string operations in audio callback caused dropouts, thread sanitizer revealed dozens of subtle data races
  • 2.1 (2018): Qt::DirectConnection threading violations systematically eliminated after discovering race conditions causing rare crashes
  • 2.2 (2019): Signal/slot connection patterns formalized after controller hot-swap crashes from missing disconnect() calls
  • 2.3 (2021): RAII and const-correctness guidelines strengthened during C++ modernization
  • Represents 20+ years of collective debugging experience distilled into actionable rules
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment