INSTRUCTIONS.md 10 KB

MixBoard Development Instructions

Project Overview

MixBoard is a native macOS music player and mixtape builder built with Swift, SwiftUI, and SwiftData. It's designed for DJs and music curators who want to listen to music, curate playlists, and export mixes for further editing in DAWs like Adobe Audition.

Core Workflow

  1. Import audio files (MP3, WAV, FLAC, M4A, AIFF, etc.) into playlists
  2. Listen and browse across multiple playlists
  3. Curate a mix by dragging tracks or using quick-add (⌘D) to a target playlist
  4. Export as an Adobe Audition session (.sesx), stitched WAV with markers, CUE sheet, EDL, DAWproject, or M3U

Key Features

  • Playback: AVAudioEngine-based player with auto-advance, shuffle, repeat, seek, next/prev
  • Playlist management: Create playlists and folders, drag tracks between playlists, drag & drop files/folders from Finder
  • Configurable playlist view: Choose which metadata columns to display (artist, title, album, BPM, key, duration, format, etc.), group by artist/album/genre/folder
  • Artwork: Loaded foobar2000-style from folder images (cover.jpg, folder.jpg) or embedded metadata
  • Skinning: 13 themes (Dark, Midnight, Forest, Ocean, Warm, Light, Winamp Classic, Winamp Modern, foobar2000, Windows 95, Windows 98, XP Luna, Mac OS 9) — all persisted
  • DAW export: Adobe Audition .sesx (real format, references original files with absolute paths), audio stitcher (concatenates to single WAV with marker files), CUE sheets, EDL, DAWproject, M3U
  • File renaming on export: Configurable template system ({artist}, {title}, {album}, {track}, {bpm}, {key}, etc.)
  • Quick-add (⌘D): Set a target playlist, press ⌘D to instantly add the playing track — with duplicate detection
  • Global search (⌘F): Search all playlists by artist/title/album
  • Track notes: Free-text notes per track, preserved in exports
  • Cursor/playback modes: "Cursor follows playback" and "Playback follows cursor" (mutually exclusive, toggled via player bar button or right-click menu)
  • State persistence: Last playlist, last track, playback position, skin, target playlist all saved/restored on relaunch
  • Media keys: Registers with MPRemoteCommandCenter for keyboard play/pause/next/prev
  • AirPlay: Handles audio route changes — resumes on device switch, pauses on disconnect
  • BPM & key detection: Spectral analysis with Accelerate framework (autocorrelation for BPM, chromagram + Krumhansl-Kessler profiles for key)
  • Waveform generation: Downsampled min/max pairs for visualization

Architecture

  • SwiftData models: Track, CuePoint, Playlist, PlaylistEntry, PlaylistFolder
  • @Observable ViewModels: PlayerViewModel (wraps AudioEngine, syncs state at 30fps), PlaylistViewModel (CRUD, quick-add, status messages)
  • Singleton configs: PlaylistViewConfig.shared, AppTheme (ObservableObject with @Published)
  • AudioEngine: AVAudioEngine + AVAudioPlayerNode, generation-counter for completion handler safety, .dataPlayedBack completion type
  • Services: MetadataService, WaveformGenerator, BPMDetector, KeyDetector, LibraryManager, ArtworkService, MediaKeyHandler

Known Issues / Quirks

  • Double-click to play in SwiftUI List is unreliable — Enter key is the primary way to play a selected track
  • PlaylistViewConfig is a singleton (PlaylistViewConfig.shared) to keep the player bar cursor-mode button and playlist view context menu in sync
  • The .sesx exporter is reverse-engineered from real Audition files but may not work with all Audition versions
  • BPM/key detection may fail on test-generated sine WAVs (tests handle this gracefully)
  • Theme tests save/restore the original skin in setUp/tearDown to avoid polluting UserDefaults

Build & Run

# Build and launch the app (no Xcode GUI needed):
cd /Users/vdrobkov/Misc/Documents/Copilot/MixBoard
pkill -f MixBoard 2>/dev/null; sleep 0.5
xcodegen generate 2>&1 | tail -1
xcodebuild -project MixBoard.xcodeproj -scheme MixBoard -destination 'platform=macOS' build 2>&1 | tail -3
open /Users/vdrobkov/Library/Developer/Xcode/DerivedData/MixBoard-hfuiayempdyihjaigvvglaqkkkth/Build/Products/Debug/MixBoard.app

Testing Rules

MANDATORY: After implementing any new feature or changing any existing feature:

  1. Write new tests covering the changes
  2. Run the full test suite and confirm all tests pass
  3. Do not consider the task complete until tests pass

Tests are split into:

  • Unit tests (Tests/Unit/) — model, service, exporter, filename template, app state tests
  • E2E integration tests (Tests/E2E/) — full workflow tests (player load/play/pause/stop/seek, playlist next/prev/shuffle/repeat, import→playlist→export, stitch with markers, theme persistence, config persistence, quick-add, duplicate detection, track notes, drag-to-playlist)

All tests run as hosted unit tests (inside the app process) — no UI automation needed. Currently 99 tests, 0 failures.

# Run all tests (unit + E2E):
xcodebuild -project MixBoard.xcodeproj -scheme MixBoardTests -destination 'platform=macOS' test 2>&1 | grep "Executed" | tail -2

# Run tests with failure details:
xcodebuild -project MixBoard.xcodeproj -scheme MixBoardTests -destination 'platform=macOS' test 2>&1 | grep -E "passed|failed|Executed" | tail -20

Regenerate Xcode Project

After adding or removing source files, always regenerate:

xcodegen generate

Project Structure

Sources/
├── MixBoardApp.swift              # App entry, menu commands, keyboard shortcuts
├── Models/
│   ├── Track.swift                # @Model — audio track with metadata
│   ├── CuePoint.swift             # @Model — markers on tracks
│   ├── Playlist.swift             # @Model — ordered collection of entries
│   ├── PlaylistFolder.swift       # @Model — groups playlists in sidebar
│   ├── PlaylistViewConfig.swift   # Column visibility, grouping, cursor mode (singleton)
│   ├── AppTheme.swift             # 13 skins, all colors/sizes (ObservableObject)
│   ├── AppState.swift             # UserDefaults persistence for last playlist/track/position
│   └── FileNameTemplate.swift     # Configurable export file naming ({artist}, {title}, etc.)
├── Services/
│   ├── AudioEngine.swift          # AVAudioEngine playback, seek, route change handling
│   ├── BPMDetector.swift          # Spectral flux + autocorrelation BPM detection
│   ├── KeyDetector.swift          # Chromagram + key profile matching
│   ├── WaveformGenerator.swift    # Downsampled min/max waveform data
│   ├── MetadataService.swift      # AVFoundation metadata reading
│   ├── LibraryManager.swift       # File import, analysis, file operations
│   ├── ArtworkService.swift       # Folder artwork loading (foobar2000 style)
│   └── MediaKeyHandler.swift      # MPRemoteCommandCenter integration
├── Export/
│   ├── DAWExporter.swift          # Protocol, ExportOptions, MixExporter dispatcher
│   ├── AuditionExporter.swift     # Real .sesx format with absolute file paths
│   ├── AudioStitcher.swift        # Concatenate to single WAV + markers CSV/CUE
│   ├── CueSheetExporter.swift     # Standard .cue format
│   ├── EDLExporter.swift          # CMX 3600 EDL
│   ├── DAWProjectExporter.swift   # Open DAWproject format
│   └── M3UExporter.swift          # M3U playlist
├── ViewModels/
│   ├── PlayerViewModel.swift      # @Observable — playback state, next/prev, shuffle/repeat
│   └── PlaylistViewModel.swift    # @Observable — CRUD, quick-add, duplicate detection, status
└── Views/
    ├── ContentView.swift          # Main layout, state restore, status toast
    ├── SidebarView.swift          # Playlists + folders, drag/drop, target playlist
    ├── PlaylistView.swift         # Track list, column headers, configurable rows, context menu
    ├── PlayerView.swift           # Transport, shuffle/repeat, cursor mode, seekbar, volume, skin picker
    ├── ExportSheet.swift          # Session export + stitch export tabs, file naming template
    ├── GlobalSearchSheet.swift    # Search across all playlists
    ├── WaveformView.swift         # Interactive waveform canvas
    ├── ArtworkView.swift          # Async folder/embedded artwork loading
    └── TrackRow.swift             # Compact track display

Tests/
├── Unit/
│   ├── ModelTests.swift           # Track, CuePoint, Playlist, PlaylistEntry, PlaylistFolder, AppTheme, AppState
│   ├── ServiceTests.swift         # MetadataService, WaveformGenerator, BPMDetector, KeyDetector
│   ├── ExporterTests.swift        # All 5 export formats
│   └── FileNameTemplateTests.swift # Template generation, variables, sanitization, presets
├── E2E/
│   ├── E2EWorkflowTests.swift     # Import→playlist→export, stitch, analysis, multi-format
│   └── IntegrationTests.swift     # Player VM flows, shuffle/repeat, export, quick-add, duplicates, notes
└── Helpers/
    └── TestHelpers.swift          # Test audio file generation (sine wave WAVs)

Coding Conventions

  • Use @Observable (not ObservableObject) for new ViewModels
  • Use @EnvironmentObject for ObservableObject types (LibraryManager, AppTheme)
  • Use @Environment for @Observable types (PlayerViewModel, PlaylistViewModel)
  • Use PlaylistViewConfig.shared — it's a singleton, don't create new instances
  • Always use Color.accentColor (not .accentColor) in ternary expressions to avoid ShapeStyle type errors
  • Prefer xcodebuild over swift build for the final build (SwiftData macros require Xcode toolchain)
  • After adding/removing files, run xcodegen generate before building
  • Theme tests must save/restore the original skin in setUp/tearDown
  • AppState tests must clear UserDefaults keys in setUp/tearDown
  • AudioEngine uses a playbackGeneration counter to prevent completion handler race conditions
  • Use .dataPlayedBack (not default .dataConsumed) for AVAudioPlayerNode completion handlers
  • Prefer xcodebuild over swift build for the final build (SwiftData macros require Xcode toolchain)