nullPlayer is a free open source macOS media player with some unique features
Swift
126
2,330 commits
updated Oct 2, 2026
https://github.com/user-attachments/assets/4f53dbd9-cfd2-4ca1-87d5-96d39e224a92
NullPlayer is 100% open source and built as a clean-room effort; no Winamp, WACUP, or Windows Media Player source code has been copied or viewed
Nullplayer is the first macOS and only open-source engine capable of rendering Winamp ClassicPro and Winamp 5 (.wal) skins, letting you run all Winamp skin types on macOS while enjoying modern media ecosystem support
NullPlayer is also the first macOS and only open-source engine capable of rendering Windows Media Player (.wmz) skins:from WMP 7, 7.1, XP (8), 9, 10, 11 and 12. It supports their views, JScript, EQ, SRS and visualizer controls, all driving NullPlayer's playback engine. Skin scripts run sandboxed in an isolated JavaScriptCore context with no ActiveX, registry, shell, DLL, plug-in or HTML access.
NullPlayer is not affiliated with, endorsed by, or connected to Winamp, Nullsoft, Microsoft, Winamp Group SA, Llama Group, Radionomy Group, Jamendo, Hotmix, Bridger, SHOUTcast, Sonos, Plex, WACUP, Emby, Jellyfin, Google or anyone else
No Winamp or Windows Media Player skins are distributed with the project
.cue sheets (requires yt-dlp + ffmpeg)yt-dlp + ffmpeg).wsz Winamp 2.X skin support with window snapping and docking.wal) skin support (Winamp 5.X and ClassicPro); the ClassicPro Windows plug-in engine is extracted from the user-supplied installer and runs natively on macOS.wmz skins, loaded through a bounded, isolated engine with a sandboxed JScript runtime; import, select, remove, and switch views from Skins > Media Player, or fall back to a built-in unskinned player.cue sheet support - virtual split cue/flac into ephemeral playlist items or optionally split to multiple tagged files on importNullPlayer's visualizations span its windows, from the in-skin main-window display to full-screen engines. All visualization choices persist across launches.:
.wal) skins — the skin draws its own in-skin visualizations, including workscope/oscilloscope analyzers and custom engine visuals (e.g. the MMD3 and ClassicPro beat visualizers).wmz) skins — a skin's <EFFECTS> area renders native Spikes, Bars, Ambience, Cava, or vis_classic visuals inside the rectangle the skin draws for themNullPlayer is built to be worked on by coding agents as well as people. The repo ships the context an agent needs instead of making it rediscover the codebase every session.
skills/ — ~44,000 lines of maintained technical documentation, one owner per subsystem: audio and EQ, each skin engine, every media-server integration, casting, each visualizer, the local library, CLI, and testing. The clean-room skin engines add per-skin dossiers (what each problem skin taught the engine and what was ruled out), a canonical probe/harness reference, and a skin-subsystem-blueprint for adding a new skin family./wal-skin-report <skin.wal> produces a full compatibility report for a Winamp 5 skin; skin-screenshots drives the live app to capture one main-window frame per skin across all four skin systems and assembles a slideshow GIF.--cli mode for querying libraries, resolving sources, starting playback, and routing to Sonos / Chromecast / DLNA, plus a --ui-testinglaunch mode and accessibility identifiers so the UI can be driven programmatically..wal and .wmz corpus render sweeps and census (with a corpus exclusion list and baseline diffing), and backlog validation, so an agent verifies its work the same way a maintainer does.live-ui-testing process skill for bugs that only show on screen: instrument first, drive the running app with AppleScript and synthesized input, and measure what is actually drawn; every subsystem skill routes its Debugging a live defect section there.Tests/, with a documented testing philosophy (skills/testing) that explicitly forbids weakening tests changing app code just to make them pass..wal) and Windows Media Player (.wmz) work must never alter Classic or Original behavior, the sprite-origin and Data-slicing gotchas are stated up front.app-control skill lets an agent launch, configure, drive, screenshot, and measure the running debug build across every skin family (Classic, Original, Original-Metal, .wal, .wmz):Download the latest DMG:
https://github.com/ad-repo/nullplayer/releases/latest/download/NullPlayer.dmg
Requires macOS 14 Sonoma or newer.
NullPlayer is not signed with an Apple Developer ID — that requires a paid Apple developer account, which this project does not have and has no plans to buy. Because of that, macOS Gatekeeper will block the app on first launch with an "app is damaged" or "cannot verify that it is free from malware" message. This is expected, not a sign anything is wrong. Clearing the quarantine flag is a required install step — run it every time you install or update via the DMG:
Open NullPlayer.dmg.
Drag NullPlayer.app to Applications.
Clear the quarantine flag so macOS will open the app. Open Terminal (Cmd + Space, type Terminal, press Return) and run:
xattr -cr /Applications/NullPlayer.app
Open NullPlayer from Applications.
See docs/download.md for the same install steps in a short download-only page.
Tip: Don't want to run a Terminal command every time you update? Install with Homebrew (next section) instead — the cask clears the quarantine flag for you automatically, so the app just opens.
Homebrew is a free package manager for macOS. This is the smoothest way to install NullPlayer: Homebrew removes the Gatekeeper quarantine flag automatically, so you never see the "app is damaged" warning, and updates are a single command.
New to Homebrew? Here's the whole thing, start to finish:
Open Terminal — press Cmd + Space, type Terminal, and press Return.
Install Homebrew by pasting this line and pressing Return. It asks for your Mac login password (the cursor stays still while you type — that's normal) and takes a few minutes:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Already have Homebrew? Skip this step.
Add Homebrew to your shell so the brew command is found. The installer finishes by printing a Next steps section — run the two commands it lists. On Apple Silicon Macs (M1/M2/M3/M4) they are:
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"
On older Intel Macs, replace /opt/homebrew with /usr/local. If brew already worked before you started, skip this step.
Add the NullPlayer tap (one-time configuration):
brew tap ad-repo/nullplayer
Install NullPlayer:
brew install --cask ad-repo/nullplayer/nullplayer
Open NullPlayer from your Applications folder or Launchpad — no security prompt.
Updating to a new release:
brew update
brew upgrade --cask ad-repo/nullplayer/nullplayer
Verify the tap is serving the latest version:
brew livecheck --cask ad-repo/nullplayer/nullplayer
brew uninstall --cask --zap nullplayer removes app data under ~/Library/Application Support/NullPlayer and the app's preferences/caches, but does not remove Keychain entries for Plex/Subsonic/Jellyfin/Emby tokens. To clear those:
security delete-generic-password -s com.nullplayer.app
If you'd rather not run the xattr command in step 3 above, you can clear the block through System Settings instead:
NullPlayer.app to Applications and double-click it once. macOS will refuse to open it — that's expected.After this NullPlayer opens normally. (Installing with Homebrew avoids this entirely — the cask clears the flag for you.)
If you want to use NullPlayer as a scriptable command in terminal workflows or automation pipelines, the DMG includes:
nullplayer — launcher wrapperInstall NullPlayer CLI.command — one-click installer for /usr/local/bin/nullplayerInstall flow:
bash "/Volumes/NullPlayer/Install NullPlayer CLI.command"
nullplayer --cli --help
The launcher looks for:
/Applications/NullPlayer.app~/Applications/NullPlayer.appThe YouTube source and Stream Ripper features download and transcode media by shelling out to two command-line tools that are not bundled — install them via Homebrew:
brew install yt-dlp ffmpeg
Postprocessing: ffmpeg not found and audio downloads can't be converted.NullPlayer looks for both in the standard Homebrew/MacPorts locations (/opt/homebrew/bin, /usr/local/bin, /opt/local/bin, /usr/bin). Keep them current with brew upgrade yt-dlp ffmpeg — an outdated yt-dlp may fail to list or download videos as YouTube changes.
Requires Xcode 15.0+ with Command Line Tools and Swift 5.9+.
# Clone the repository
git clone https://github.com/ad-repo/nullplayer.git
cd nullplayer
# Download required frameworks
./scripts/bootstrap.sh
# Build and run
./scripts/kill_build_run.sh
The bootstrap script downloads VLCKit and libprojectM from GitHub Releases with checksum verification.
To open in Xcode:
open Package.swift
| Library | Purpose |
|---|---|
| ZIPFoundation | .wsz / .wmz skin file extraction |
| SQLite.swift | Media library storage |
| AudioStreaming | HTTP audio streaming for Plex |
| FlyingFox | Embedded HTTP server for local file casting |
| libprojectM | ProjectM visualizations |
Library data is stored as a SQLite database at ~/Library/Application Support/NullPlayer/library.db.
Backup & Restore API (MediaLibrary.swift):
| Function | Description |
|---|---|
backupLibrary(customName:) | Creates timestamped .db backup, returns URL |
restoreLibrary(from:) | Restores from backup (auto-backs up current first) |
listBackups() | Returns backup URLs sorted newest first |
deleteBackup(at:) | Deletes a backup file |
Backups are stored in ~/Library/Application Support/NullPlayer/Backups/.
NullPlayer has five looks — Modern, Classic, Original, Original-Metal, and Windows Media Player — selectable from the right-click context menu under Skins. Switching between them happens live, with no restart — playback, casting, and the open playlist continue uninterrupted while the windows rebuild in the new look:
.wal) / ClassicProClassicPro skins (such as cPro-Bento, cPro2 Dark Aluminum, and the cPro Insomnis family) depend on the ClassicPro plug-in, which ships in a Windows installer. NullPlayer extracts that installer internally — no Wine, no external tools — but you must supply the installer. You can download it directly from Skins > Modern > Download ClassicPro Engine... and that will open a browser to the Softpedia download page.
Only ClassicPro_2.01.exe is supported. Other versions or builds are not recognized; importing them shows a clear warning and the engine stays inactive.
To set up:
ClassicPro_2.01.exe.NullPlayer's Winamp Modern support is built through clean-room reverse engineering, verified against a corpus of real-world skins. The skin compatibility report is the current corpus of .wal skins tested in that effort — every Winamp 5.x skin the engine has been measured against, its compatibility grade, and what is still known to be outstanding on it. It is updated as the engine improves and new skins are measured.
.wmz)NullPlayer is the only Mac app that runs Windows Media Player skins. The .wmz skins made for WMP 7 through 12 — shaped windows, multiple views, compact/full modes, and the JScript that animates them — load in a clean-room engine and drive NullPlayer's own playback, EQ, SRS enhancements, and visualizers. A fresh install opens in NullPlayer's built-in unskinned WMP player until you import a skin.
Skin scripts run in a restricted, sandboxed object model: ActiveX, registry and shell access, DLLs, WMP plug-ins, and skin-authored HTML are not supported. See the WMP skin guide and the compatibility reference for the supported surface.
Classic .wsz skin support. The app starts with a native macOS appearance and ships with one original NullPlayer skin (Silver). To apply a skin, use Skins > Load Skin... to open a .wsz file, or place skin files in ~/Library/Application Support/NullPlayer/Skins/ and select them from the Skins menu. Thousands of community-created skins can be downloaded from the Skins > Get More Skins... menu link, which opens the Winamp Skin Museum.
A custom skin engine built from scratch with a neon cyberpunk aesthetic. Original skins are JSON-configured and support:
The bundled default skin ("NeonWave") is fully programmatic -- zero image assets, pure palette-driven rendering.
Creating a skin is as simple as writing a single JSON file. See SKINNING.md for the complete guide.
Skin installation: Place skin folders or .nsz bundles in the compatibility directory ~/Library/Application Support/NullPlayer/ModernSkins/, then right-click the player and select your skin from Skins > Original.
A hi-fi hardware faceplate look, selected from Skins > Original-Metal, with seven finishes — Brushed Steel, Aluminum, Gunmetal, Anodized Black, Brass, Bronze, and Copper. Each finish restyles the whole player (chrome, panels, sliders, transport, and EQ) with a backlit-green LCD for the time and track displays and a spectrum analyzer matched to the finish.
NullPlayer includes a first-class headless CLI mode for browsing, querying, playing, and routing media entirely from the terminal. It is designed to work as a scriptable command in automation pipelines: resolve media from multiple sources, pick a local output or cast target, then hand off playback without opening the GUI.
This is not just a hidden debug flag. nullplayer is a supported command surface for:
--jsonnullplayer --cli [OPTIONS]
nullplayer can act as a scriptable media control command for automation pipelines, connecting multiple media sources to multiple playback targets.
Supported media sources include:
Supported playback targets include:
Typical automation shape:
nullplayer --cli --source plex --playlist "All Music" --cast "Living Room" --cast-type sonos
nullplayer --cli --source local --artist "Courtney Barnett" --output "MacBook Pro Speakers"
nullplayer --cli --source radio --station "Radio Paradise: Mellow Mix" --cast "Kitchen Speaker" --cast-type chromecast
nullplayer --cli --source plex --library Movies --movie "Alien: Romulus" --cast "Living Room TV" --cast-type chromecast
nullplayer --cli --file "/path/to/video.mkv" --cast "Samsung QN90BA 75" --cast-type dlna
Audio and video scope. Music playback supports local output, Sonos, Chromecast, and DLNA targets. Video is cast-only in CLI mode: use
--filefor local video files or--movie/--show/--episodefor Plex, Jellyfin, and Emby video libraries. Video requires--castand supports Chromecast or DLNA TV targets; Sonos is audio-only.
nullplayer --cli --list-sources # show configured sources
nullplayer --cli --list-libraries --source plex # list Plex libraries
nullplayer --cli --list-artists --source plex --library AD-FLAC # list artists in a Plex library
nullplayer --cli --list-albums --source plex --library AD-FLAC --artist "Soundgarden"
nullplayer --cli --list-albums --source local # list local albums
nullplayer --cli --list-tracks --source local --artist "Rush" # list tracks
nullplayer --cli --list-genres --source local # list local genres
nullplayer --cli --list-artists --source subsonic # Navidrome/Subsonic artists
nullplayer --cli --list-albums --source jellyfin --artist "3rd Bass" # Jellyfin albums by artist
nullplayer --cli --list-libraries --source emby # Emby libraries
nullplayer --cli --list-playlists --source plex # list playlists
nullplayer --cli --list-stations # list internet radio stations
nullplayer --cli --list-eq # list EQ presets
nullplayer --cli --list-outputs # list audio output devices
nullplayer --cli --list-devices # list cast devices
nullplayer --cli --search "soundgarden" --source plex --library AD-FLAC # search a library
nullplayer --cli --list-artists --source plex --library AD-FLAC --json # JSON output
Selecting a Plex library. Plex servers often expose several music libraries (e.g.
AD-FLAC,AD-MP3,Classical-FLAC). Pass--library <name>to pick one. If you omit it, the CLI uses your last-selected music library; if that is ambiguous it prints the available music libraries so you can choose. Runnullplayer --cli --list-libraries --source plexto see the exact names.
# Local library
nullplayer --cli --source local --artist "Courtney Barnett"
nullplayer --cli --source local --album "Dub Side Of The Moon"
nullplayer --cli --source local --genre "Reggae" --repeat-all
nullplayer --cli --source local --artist "Rush" --shuffle
# Plex — pick a music library with --library (omit to use your last-selected one)
nullplayer --cli --source plex --library AD-FLAC --artist "Soundgarden" --album "SuperUnknown"
nullplayer --cli --source plex --library AD-FLAC --artist "AC/DC" --shuffle
nullplayer --cli --source plex --library AD-FLAC --artist "AC/DC" --album "Black Ice" --tuning 432
nullplayer --cli --source plex --playlist "All Music"
# Subsonic / Navidrome (music-only server; --library selects a music folder)
nullplayer --cli --source subsonic --artist "ZZ Top" --album "Eliminator"
nullplayer --cli --source subsonic --artist "ZZ Top" --shuffle
# Jellyfin (--library selects a music library; omit to use the current one)
nullplayer --cli --source jellyfin --library "Music" --artist "3rd Bass" --album "The Cactus Album"
nullplayer --cli --source jellyfin --artist "3rd Bass" --shuffle
# Emby
nullplayer --cli --source emby --library "Music" --artist "ZZ Top" --album "La Futura"
nullplayer --cli --source emby --artist "ZZ Top"
# Internet radio
nullplayer --cli --source radio --station "Radio Paradise: Mellow Mix"
nullplayer --cli --source radio --station "Heart 80s UK"
# Outputs and casting
nullplayer --cli --source local --artist "Augustus Pablo" --output "MacBook Pro Speakers"
nullplayer --cli --source plex --playlist "Recently Added" --cast "Living Room" --cast-type sonos
nullplayer --cli --source plex --library AD-FLAC --artist "Soundgarden" --album "Louder Than Love" --library AD-FLAC --cast "Dining Room" --cast-type sonos
# Sonos multi-room: the first name is the group coordinator, the rest are grouped onto it
nullplayer --cli --source plex --playlist "Recently Added" --cast "Living Room,Kitchen,Office" --cast-type sonos
# (equivalent to --cast "Living Room" --sonos-rooms "Kitchen,Office")
Video commands require --cast and route to Chromecast or DLNA TV targets. Use nullplayer --cli --list-devices to get the exact device names on your network.
Local video files are served through NullPlayer's embedded local media server on port 8765. If the main NullPlayer app is already open, it may already own that port; quit the app UI or stop the other NullPlayer process before retrying the CLI cast. Videos added with Add Video Files... stay at their original file paths; cast them from the CLI with --file.
# Local video file
nullplayer --cli --file "/path/to/video.mkv" --cast "Chromecast-Ultra-91a086b76d0c422dd9dd9cda078e4911" --cast-type chromecast
nullplayer --cli --file "/path/to/video.mkv" --cast "Samsung QN90BA 75" --cast-type dlna
# Local video file from Downloads
nullplayer --cli --file "$HOME/Downloads/My Movie.mp4" --cast "Chromecast-Ultra-91a086b76d0c422dd9dd9cda078e4911" --cast-type chromecast --verbose
nullplayer --cli --file "$HOME/Downloads/My Movie.mp4" --cast "Samsung QN90BA 75" --cast-type dlna --verbose
# Plex movies
nullplayer --cli --source plex --library Movies --movie "Alien: Romulus" --cast "Chromecast-Ultra-91a086b76d0c422dd9dd9cda078e4911" --cast-type chromecast
nullplayer --cli --source plex --library Movies --movie "Alien: Romulus" --cast "Samsung QN90BA 75" --cast-type dlna
# Plex TV episodes
nullplayer --cli --source plex --library "TV Shows" --show "Alien: Earth" --episode "Neverland" --season 1 --number 1 --cast "Chromecast-Ultra-91a086b76d0c422dd9dd9cda078e4911" --cast-type chromecast
nullplayer --cli --source plex --library "TV Shows" --show "Alien: Earth" --episode "Neverland" --season 1 --number 1 --cast "Samsung QN90BA 75" --cast-type dlna
# Emby movies
nullplayer --cli --source emby --library Movies --movie "Alien: Romulus" --cast "Chromecast-Ultra-91a086b76d0c422dd9dd9cda078e4911" --cast-type chromecast
nullplayer --cli --source emby --library Movies --movie "Alien: Romulus" --cast "Samsung QN90BA 75" --cast-type dlna
# Emby TV episodes
nullplayer --cli --source emby --library "TV shows" --show "Abbott Elementary" --episode "Ava & Fest" --season 5 --number 21 --cast "Chromecast-Ultra-91a086b76d0c422dd9dd9cda078e4911" --cast-type chromecast
nullplayer --cli --source emby --library "TV shows" --show "Abbott Elementary" --episode "Ava & Fest" --season 5 --number 21 --cast "Samsung QN90BA 75" --cast-type dlna
# Jellyfin movies and TV episodes
nullplayer --cli --source jellyfin --library "Movies" --movie "Alien: Romulus" --cast "Chromecast-Ultra-91a086b76d0c422dd9dd9cda078e4911" --cast-type chromecast
nullplayer --cli --source jellyfin --library "Movies" --movie "Alien: Romulus" --cast "Samsung QN90BA 75" --cast-type dlna
nullplayer --cli --source jellyfin --library "TV Shows" --show "Alien: Earth" --episode "Neverland" --season 1 --number 1 --cast "Chromecast-Ultra-91a086b76d0c422dd9dd9cda078e4911" --cast-type chromecast
nullplayer --cli --source jellyfin --library "TV Shows" --show "Alien: Earth" --episode "Neverland" --season 1 --number 1 --cast "Samsung QN90BA 75" --cast-type dlna
DLNA video devices do not report reliable end-of-stream status, so press q to stop the CLI when the video ends. Chromecast video exits automatically after playback ends and the cast session is torn down.
Set the initial playback volume at launch:
nullplayer --cli --source local --artist "Rush" --volume 80
nullplayer --cli --source plex --playlist "All Music" --cast "Living Room" --cast-type sonos --volume 35
During playback:
↑ increases volume by 5%↓ decreases volume by 5%m toggles muteFor audio casting, the same CLI volume control path is used for the cast target as well.
Reference Tuning pitch-shifts local output to a selected reference frequency, such as retuning A=440 content to A=432. In the app, use Playback > Options > Reference Tuning for Off, 432 Hz, 440 Hz, or custom source/target Hz. It applies to local files and HTTP streams from Plex, Subsonic/Navidrome, Jellyfin, Emby, and internet radio. It is unavailable while casting because Sonos, Chromecast, and DLNA renderers receive the media URL directly.
CLI overrides are session-only:
nullplayer --cli --source local --artist "Rush" --tuning 432
nullplayer --cli --source plex --library AD-FLAC --artist "Soundgarden" --tuning 432 --tuning-source 440
nullplayer --cli --source radio --station "Radio Paradise: Mellow Mix" --tuning-offset-cents -31.766
| Key | Action |
|---|---|
Space | Pause/Resume |
q | Quit |
> / < | Next / Previous track |
→ / ← | Seek forward / backward 10s |
↑ / ↓ | Volume up / down |
s | Toggle shuffle |
r | Cycle repeat (off → all → one) |
m | Toggle mute |
i | Show track info |
For video casting, Space pauses/resumes the cast, → / ← seek on the cast session, and q stops casting before exiting. Track navigation, shuffle, repeat, mute, and volume controls are audio-only.
During music playback the CLI shows album art in the terminal. The render mode is auto-detected from the terminal's color support and can be forced:
--color-art: force color art--ascii-art: force the monochrome character-ramp — use this if your terminal reports color but renders the art as flat blocks--no-art: disable album artnullplayer --cli --source local --artist "Rush" --ascii-art
If a terminal misreports its color support (some shell profiles export COLORTERM=truecolor globally, making every terminal claim color it can't paint), set a per-terminal default in that terminal's shell profile instead of passing a flag each time:
export NULLPLAYER_ART=ascii # or: color, auto (default). Flags still override.
Framework log output is suppressed by default so the session stays clean. Pass --verbose to keep it for debugging:
nullplayer --cli --source plex --playlist "All Music" --cast "Living Room" --cast-type sonos --verbose
See nullplayer --cli --help for the full flag reference.
See AGENTS.md for documentation links and key source files.
Note: This project will never support Spotify, Youtube, Apple or Amazon. Please do not submit PRs for this type of integration.
NullPlayer does not collect or transmit personal data to the developer. Playback and usage history is stored only in the app's local SQLite database. See the Privacy Policy for details about local storage and user-directed network features.
This project is open source. Because it bundles GPL-licensed components (aubio and the PeppyMeter meter templates), the combined application is distributed under the terms of the GNU GPL v3.0 only. (The video engine is VLCKit/libVLC, which is LGPL — see below.)
The NullPlayer name, logo, icon, and other brand identifiers are not licensed for use by modified distributions. Forks, derivative works, and redistributed builds must use a different application name and replace or remove NullPlayer branding from user-facing product names, bundle names, bundle identifiers, executable names, icons, and public marketing materials unless they have prior written permission. Accurate attribution such as "based on NullPlayer" is allowed when it does not imply endorsement.
The full text of every third-party notice ships inside the app bundle at
Contents/Resources/ThirdPartyLicenses/ (aggregated in ThirdPartyNotices.txt,
with the individual license texts alongside it). scripts/build_dmg.sh runs
scripts/validate_notices.sh to fail the release if any bundled dependency is
missing its notice. See docs/third-party-notices.md
for the refresh process and scripts/third_party_components.tsv for the
authoritative component/version/license list.
Bundled third-party components:
Swift packages (compiled into the binary)
.wsz/.nps/.wmz extractionBundled frameworks / dynamic libraries
Native visualization ports (compiled into the binary)
Fonts & assets
31 followers · starred Apr 2026
Swift
89.5%
C++
6.9%
Shell
1.3%
nullPlayer is a free open source macOS media player with some unique features
Swift
126
2,330 commits
updated Oct 2, 2026
https://github.com/user-attachments/assets/4f53dbd9-cfd2-4ca1-87d5-96d39e224a92
NullPlayer is 100% open source and built as a clean-room effort; no Winamp, WACUP, or Windows Media Player source code has been copied or viewed
Nullplayer is the first macOS and only open-source engine capable of rendering Winamp ClassicPro and Winamp 5 (.wal) skins, letting you run all Winamp skin types on macOS while enjoying modern media ecosystem support
NullPlayer is also the first macOS and only open-source engine capable of rendering Windows Media Player (.wmz) skins:from WMP 7, 7.1, XP (8), 9, 10, 11 and 12. It supports their views, JScript, EQ, SRS and visualizer controls, all driving NullPlayer's playback engine. Skin scripts run sandboxed in an isolated JavaScriptCore context with no ActiveX, registry, shell, DLL, plug-in or HTML access.
NullPlayer is not affiliated with, endorsed by, or connected to Winamp, Nullsoft, Microsoft, Winamp Group SA, Llama Group, Radionomy Group, Jamendo, Hotmix, Bridger, SHOUTcast, Sonos, Plex, WACUP, Emby, Jellyfin, Google or anyone else
No Winamp or Windows Media Player skins are distributed with the project
.cue sheets (requires yt-dlp + ffmpeg)yt-dlp + ffmpeg).wsz Winamp 2.X skin support with window snapping and docking.wal) skin support (Winamp 5.X and ClassicPro); the ClassicPro Windows plug-in engine is extracted from the user-supplied installer and runs natively on macOS.wmz skins, loaded through a bounded, isolated engine with a sandboxed JScript runtime; import, select, remove, and switch views from Skins > Media Player, or fall back to a built-in unskinned player.cue sheet support - virtual split cue/flac into ephemeral playlist items or optionally split to multiple tagged files on importNullPlayer's visualizations span its windows, from the in-skin main-window display to full-screen engines. All visualization choices persist across launches.:
.wal) skins — the skin draws its own in-skin visualizations, including workscope/oscilloscope analyzers and custom engine visuals (e.g. the MMD3 and ClassicPro beat visualizers).wmz) skins — a skin's <EFFECTS> area renders native Spikes, Bars, Ambience, Cava, or vis_classic visuals inside the rectangle the skin draws for themNullPlayer is built to be worked on by coding agents as well as people. The repo ships the context an agent needs instead of making it rediscover the codebase every session.
skills/ — ~44,000 lines of maintained technical documentation, one owner per subsystem: audio and EQ, each skin engine, every media-server integration, casting, each visualizer, the local library, CLI, and testing. The clean-room skin engines add per-skin dossiers (what each problem skin taught the engine and what was ruled out), a canonical probe/harness reference, and a skin-subsystem-blueprint for adding a new skin family./wal-skin-report <skin.wal> produces a full compatibility report for a Winamp 5 skin; skin-screenshots drives the live app to capture one main-window frame per skin across all four skin systems and assembles a slideshow GIF.--cli mode for querying libraries, resolving sources, starting playback, and routing to Sonos / Chromecast / DLNA, plus a --ui-testinglaunch mode and accessibility identifiers so the UI can be driven programmatically..wal and .wmz corpus render sweeps and census (with a corpus exclusion list and baseline diffing), and backlog validation, so an agent verifies its work the same way a maintainer does.live-ui-testing process skill for bugs that only show on screen: instrument first, drive the running app with AppleScript and synthesized input, and measure what is actually drawn; every subsystem skill routes its Debugging a live defect section there.Tests/, with a documented testing philosophy (skills/testing) that explicitly forbids weakening tests changing app code just to make them pass..wal) and Windows Media Player (.wmz) work must never alter Classic or Original behavior, the sprite-origin and Data-slicing gotchas are stated up front.app-control skill lets an agent launch, configure, drive, screenshot, and measure the running debug build across every skin family (Classic, Original, Original-Metal, .wal, .wmz):Download the latest DMG:
https://github.com/ad-repo/nullplayer/releases/latest/download/NullPlayer.dmg
Requires macOS 14 Sonoma or newer.
NullPlayer is not signed with an Apple Developer ID — that requires a paid Apple developer account, which this project does not have and has no plans to buy. Because of that, macOS Gatekeeper will block the app on first launch with an "app is damaged" or "cannot verify that it is free from malware" message. This is expected, not a sign anything is wrong. Clearing the quarantine flag is a required install step — run it every time you install or update via the DMG:
Open NullPlayer.dmg.
Drag NullPlayer.app to Applications.
Clear the quarantine flag so macOS will open the app. Open Terminal (Cmd + Space, type Terminal, press Return) and run:
xattr -cr /Applications/NullPlayer.app
Open NullPlayer from Applications.
See docs/download.md for the same install steps in a short download-only page.
Tip: Don't want to run a Terminal command every time you update? Install with Homebrew (next section) instead — the cask clears the quarantine flag for you automatically, so the app just opens.
Homebrew is a free package manager for macOS. This is the smoothest way to install NullPlayer: Homebrew removes the Gatekeeper quarantine flag automatically, so you never see the "app is damaged" warning, and updates are a single command.
New to Homebrew? Here's the whole thing, start to finish:
Open Terminal — press Cmd + Space, type Terminal, and press Return.
Install Homebrew by pasting this line and pressing Return. It asks for your Mac login password (the cursor stays still while you type — that's normal) and takes a few minutes:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Already have Homebrew? Skip this step.
Add Homebrew to your shell so the brew command is found. The installer finishes by printing a Next steps section — run the two commands it lists. On Apple Silicon Macs (M1/M2/M3/M4) they are:
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"
On older Intel Macs, replace /opt/homebrew with /usr/local. If brew already worked before you started, skip this step.
Add the NullPlayer tap (one-time configuration):
brew tap ad-repo/nullplayer
Install NullPlayer:
brew install --cask ad-repo/nullplayer/nullplayer
Open NullPlayer from your Applications folder or Launchpad — no security prompt.
Updating to a new release:
brew update
brew upgrade --cask ad-repo/nullplayer/nullplayer
Verify the tap is serving the latest version:
brew livecheck --cask ad-repo/nullplayer/nullplayer
brew uninstall --cask --zap nullplayer removes app data under ~/Library/Application Support/NullPlayer and the app's preferences/caches, but does not remove Keychain entries for Plex/Subsonic/Jellyfin/Emby tokens. To clear those:
security delete-generic-password -s com.nullplayer.app
If you'd rather not run the xattr command in step 3 above, you can clear the block through System Settings instead:
NullPlayer.app to Applications and double-click it once. macOS will refuse to open it — that's expected.After this NullPlayer opens normally. (Installing with Homebrew avoids this entirely — the cask clears the flag for you.)
If you want to use NullPlayer as a scriptable command in terminal workflows or automation pipelines, the DMG includes:
nullplayer — launcher wrapperInstall NullPlayer CLI.command — one-click installer for /usr/local/bin/nullplayerInstall flow:
bash "/Volumes/NullPlayer/Install NullPlayer CLI.command"
nullplayer --cli --help
The launcher looks for:
/Applications/NullPlayer.app~/Applications/NullPlayer.appThe YouTube source and Stream Ripper features download and transcode media by shelling out to two command-line tools that are not bundled — install them via Homebrew:
brew install yt-dlp ffmpeg
Postprocessing: ffmpeg not found and audio downloads can't be converted.NullPlayer looks for both in the standard Homebrew/MacPorts locations (/opt/homebrew/bin, /usr/local/bin, /opt/local/bin, /usr/bin). Keep them current with brew upgrade yt-dlp ffmpeg — an outdated yt-dlp may fail to list or download videos as YouTube changes.
Requires Xcode 15.0+ with Command Line Tools and Swift 5.9+.
# Clone the repository
git clone https://github.com/ad-repo/nullplayer.git
cd nullplayer
# Download required frameworks
./scripts/bootstrap.sh
# Build and run
./scripts/kill_build_run.sh
The bootstrap script downloads VLCKit and libprojectM from GitHub Releases with checksum verification.
To open in Xcode:
open Package.swift
| Library | Purpose |
|---|---|
| ZIPFoundation | .wsz / .wmz skin file extraction |
| SQLite.swift | Media library storage |
| AudioStreaming | HTTP audio streaming for Plex |
| FlyingFox | Embedded HTTP server for local file casting |
| libprojectM | ProjectM visualizations |
Library data is stored as a SQLite database at ~/Library/Application Support/NullPlayer/library.db.
Backup & Restore API (MediaLibrary.swift):
| Function | Description |
|---|---|
backupLibrary(customName:) | Creates timestamped .db backup, returns URL |
restoreLibrary(from:) | Restores from backup (auto-backs up current first) |
listBackups() | Returns backup URLs sorted newest first |
deleteBackup(at:) | Deletes a backup file |
Backups are stored in ~/Library/Application Support/NullPlayer/Backups/.
NullPlayer has five looks — Modern, Classic, Original, Original-Metal, and Windows Media Player — selectable from the right-click context menu under Skins. Switching between them happens live, with no restart — playback, casting, and the open playlist continue uninterrupted while the windows rebuild in the new look:
.wal) / ClassicProClassicPro skins (such as cPro-Bento, cPro2 Dark Aluminum, and the cPro Insomnis family) depend on the ClassicPro plug-in, which ships in a Windows installer. NullPlayer extracts that installer internally — no Wine, no external tools — but you must supply the installer. You can download it directly from Skins > Modern > Download ClassicPro Engine... and that will open a browser to the Softpedia download page.
Only ClassicPro_2.01.exe is supported. Other versions or builds are not recognized; importing them shows a clear warning and the engine stays inactive.
To set up:
ClassicPro_2.01.exe.NullPlayer's Winamp Modern support is built through clean-room reverse engineering, verified against a corpus of real-world skins. The skin compatibility report is the current corpus of .wal skins tested in that effort — every Winamp 5.x skin the engine has been measured against, its compatibility grade, and what is still known to be outstanding on it. It is updated as the engine improves and new skins are measured.
.wmz)NullPlayer is the only Mac app that runs Windows Media Player skins. The .wmz skins made for WMP 7 through 12 — shaped windows, multiple views, compact/full modes, and the JScript that animates them — load in a clean-room engine and drive NullPlayer's own playback, EQ, SRS enhancements, and visualizers. A fresh install opens in NullPlayer's built-in unskinned WMP player until you import a skin.
Skin scripts run in a restricted, sandboxed object model: ActiveX, registry and shell access, DLLs, WMP plug-ins, and skin-authored HTML are not supported. See the WMP skin guide and the compatibility reference for the supported surface.
Classic .wsz skin support. The app starts with a native macOS appearance and ships with one original NullPlayer skin (Silver). To apply a skin, use Skins > Load Skin... to open a .wsz file, or place skin files in ~/Library/Application Support/NullPlayer/Skins/ and select them from the Skins menu. Thousands of community-created skins can be downloaded from the Skins > Get More Skins... menu link, which opens the Winamp Skin Museum.
A custom skin engine built from scratch with a neon cyberpunk aesthetic. Original skins are JSON-configured and support:
The bundled default skin ("NeonWave") is fully programmatic -- zero image assets, pure palette-driven rendering.
Creating a skin is as simple as writing a single JSON file. See SKINNING.md for the complete guide.
Skin installation: Place skin folders or .nsz bundles in the compatibility directory ~/Library/Application Support/NullPlayer/ModernSkins/, then right-click the player and select your skin from Skins > Original.
A hi-fi hardware faceplate look, selected from Skins > Original-Metal, with seven finishes — Brushed Steel, Aluminum, Gunmetal, Anodized Black, Brass, Bronze, and Copper. Each finish restyles the whole player (chrome, panels, sliders, transport, and EQ) with a backlit-green LCD for the time and track displays and a spectrum analyzer matched to the finish.
NullPlayer includes a first-class headless CLI mode for browsing, querying, playing, and routing media entirely from the terminal. It is designed to work as a scriptable command in automation pipelines: resolve media from multiple sources, pick a local output or cast target, then hand off playback without opening the GUI.
This is not just a hidden debug flag. nullplayer is a supported command surface for:
--jsonnullplayer --cli [OPTIONS]
nullplayer can act as a scriptable media control command for automation pipelines, connecting multiple media sources to multiple playback targets.
Supported media sources include:
Supported playback targets include:
Typical automation shape:
nullplayer --cli --source plex --playlist "All Music" --cast "Living Room" --cast-type sonos
nullplayer --cli --source local --artist "Courtney Barnett" --output "MacBook Pro Speakers"
nullplayer --cli --source radio --station "Radio Paradise: Mellow Mix" --cast "Kitchen Speaker" --cast-type chromecast
nullplayer --cli --source plex --library Movies --movie "Alien: Romulus" --cast "Living Room TV" --cast-type chromecast
nullplayer --cli --file "/path/to/video.mkv" --cast "Samsung QN90BA 75" --cast-type dlna
Audio and video scope. Music playback supports local output, Sonos, Chromecast, and DLNA targets. Video is cast-only in CLI mode: use
--filefor local video files or--movie/--show/--episodefor Plex, Jellyfin, and Emby video libraries. Video requires--castand supports Chromecast or DLNA TV targets; Sonos is audio-only.
nullplayer --cli --list-sources # show configured sources
nullplayer --cli --list-libraries --source plex # list Plex libraries
nullplayer --cli --list-artists --source plex --library AD-FLAC # list artists in a Plex library
nullplayer --cli --list-albums --source plex --library AD-FLAC --artist "Soundgarden"
nullplayer --cli --list-albums --source local # list local albums
nullplayer --cli --list-tracks --source local --artist "Rush" # list tracks
nullplayer --cli --list-genres --source local # list local genres
nullplayer --cli --list-artists --source subsonic # Navidrome/Subsonic artists
nullplayer --cli --list-albums --source jellyfin --artist "3rd Bass" # Jellyfin albums by artist
nullplayer --cli --list-libraries --source emby # Emby libraries
nullplayer --cli --list-playlists --source plex # list playlists
nullplayer --cli --list-stations # list internet radio stations
nullplayer --cli --list-eq # list EQ presets
nullplayer --cli --list-outputs # list audio output devices
nullplayer --cli --list-devices # list cast devices
nullplayer --cli --search "soundgarden" --source plex --library AD-FLAC # search a library
nullplayer --cli --list-artists --source plex --library AD-FLAC --json # JSON output
Selecting a Plex library. Plex servers often expose several music libraries (e.g.
AD-FLAC,AD-MP3,Classical-FLAC). Pass--library <name>to pick one. If you omit it, the CLI uses your last-selected music library; if that is ambiguous it prints the available music libraries so you can choose. Runnullplayer --cli --list-libraries --source plexto see the exact names.
# Local library
nullplayer --cli --source local --artist "Courtney Barnett"
nullplayer --cli --source local --album "Dub Side Of The Moon"
nullplayer --cli --source local --genre "Reggae" --repeat-all
nullplayer --cli --source local --artist "Rush" --shuffle
# Plex — pick a music library with --library (omit to use your last-selected one)
nullplayer --cli --source plex --library AD-FLAC --artist "Soundgarden" --album "SuperUnknown"
nullplayer --cli --source plex --library AD-FLAC --artist "AC/DC" --shuffle
nullplayer --cli --source plex --library AD-FLAC --artist "AC/DC" --album "Black Ice" --tuning 432
nullplayer --cli --source plex --playlist "All Music"
# Subsonic / Navidrome (music-only server; --library selects a music folder)
nullplayer --cli --source subsonic --artist "ZZ Top" --album "Eliminator"
nullplayer --cli --source subsonic --artist "ZZ Top" --shuffle
# Jellyfin (--library selects a music library; omit to use the current one)
nullplayer --cli --source jellyfin --library "Music" --artist "3rd Bass" --album "The Cactus Album"
nullplayer --cli --source jellyfin --artist "3rd Bass" --shuffle
# Emby
nullplayer --cli --source emby --library "Music" --artist "ZZ Top" --album "La Futura"
nullplayer --cli --source emby --artist "ZZ Top"
# Internet radio
nullplayer --cli --source radio --station "Radio Paradise: Mellow Mix"
nullplayer --cli --source radio --station "Heart 80s UK"
# Outputs and casting
nullplayer --cli --source local --artist "Augustus Pablo" --output "MacBook Pro Speakers"
nullplayer --cli --source plex --playlist "Recently Added" --cast "Living Room" --cast-type sonos
nullplayer --cli --source plex --library AD-FLAC --artist "Soundgarden" --album "Louder Than Love" --library AD-FLAC --cast "Dining Room" --cast-type sonos
# Sonos multi-room: the first name is the group coordinator, the rest are grouped onto it
nullplayer --cli --source plex --playlist "Recently Added" --cast "Living Room,Kitchen,Office" --cast-type sonos
# (equivalent to --cast "Living Room" --sonos-rooms "Kitchen,Office")
Video commands require --cast and route to Chromecast or DLNA TV targets. Use nullplayer --cli --list-devices to get the exact device names on your network.
Local video files are served through NullPlayer's embedded local media server on port 8765. If the main NullPlayer app is already open, it may already own that port; quit the app UI or stop the other NullPlayer process before retrying the CLI cast. Videos added with Add Video Files... stay at their original file paths; cast them from the CLI with --file.
# Local video file
nullplayer --cli --file "/path/to/video.mkv" --cast "Chromecast-Ultra-91a086b76d0c422dd9dd9cda078e4911" --cast-type chromecast
nullplayer --cli --file "/path/to/video.mkv" --cast "Samsung QN90BA 75" --cast-type dlna
# Local video file from Downloads
nullplayer --cli --file "$HOME/Downloads/My Movie.mp4" --cast "Chromecast-Ultra-91a086b76d0c422dd9dd9cda078e4911" --cast-type chromecast --verbose
nullplayer --cli --file "$HOME/Downloads/My Movie.mp4" --cast "Samsung QN90BA 75" --cast-type dlna --verbose
# Plex movies
nullplayer --cli --source plex --library Movies --movie "Alien: Romulus" --cast "Chromecast-Ultra-91a086b76d0c422dd9dd9cda078e4911" --cast-type chromecast
nullplayer --cli --source plex --library Movies --movie "Alien: Romulus" --cast "Samsung QN90BA 75" --cast-type dlna
# Plex TV episodes
nullplayer --cli --source plex --library "TV Shows" --show "Alien: Earth" --episode "Neverland" --season 1 --number 1 --cast "Chromecast-Ultra-91a086b76d0c422dd9dd9cda078e4911" --cast-type chromecast
nullplayer --cli --source plex --library "TV Shows" --show "Alien: Earth" --episode "Neverland" --season 1 --number 1 --cast "Samsung QN90BA 75" --cast-type dlna
# Emby movies
nullplayer --cli --source emby --library Movies --movie "Alien: Romulus" --cast "Chromecast-Ultra-91a086b76d0c422dd9dd9cda078e4911" --cast-type chromecast
nullplayer --cli --source emby --library Movies --movie "Alien: Romulus" --cast "Samsung QN90BA 75" --cast-type dlna
# Emby TV episodes
nullplayer --cli --source emby --library "TV shows" --show "Abbott Elementary" --episode "Ava & Fest" --season 5 --number 21 --cast "Chromecast-Ultra-91a086b76d0c422dd9dd9cda078e4911" --cast-type chromecast
nullplayer --cli --source emby --library "TV shows" --show "Abbott Elementary" --episode "Ava & Fest" --season 5 --number 21 --cast "Samsung QN90BA 75" --cast-type dlna
# Jellyfin movies and TV episodes
nullplayer --cli --source jellyfin --library "Movies" --movie "Alien: Romulus" --cast "Chromecast-Ultra-91a086b76d0c422dd9dd9cda078e4911" --cast-type chromecast
nullplayer --cli --source jellyfin --library "Movies" --movie "Alien: Romulus" --cast "Samsung QN90BA 75" --cast-type dlna
nullplayer --cli --source jellyfin --library "TV Shows" --show "Alien: Earth" --episode "Neverland" --season 1 --number 1 --cast "Chromecast-Ultra-91a086b76d0c422dd9dd9cda078e4911" --cast-type chromecast
nullplayer --cli --source jellyfin --library "TV Shows" --show "Alien: Earth" --episode "Neverland" --season 1 --number 1 --cast "Samsung QN90BA 75" --cast-type dlna
DLNA video devices do not report reliable end-of-stream status, so press q to stop the CLI when the video ends. Chromecast video exits automatically after playback ends and the cast session is torn down.
Set the initial playback volume at launch:
nullplayer --cli --source local --artist "Rush" --volume 80
nullplayer --cli --source plex --playlist "All Music" --cast "Living Room" --cast-type sonos --volume 35
During playback:
↑ increases volume by 5%↓ decreases volume by 5%m toggles muteFor audio casting, the same CLI volume control path is used for the cast target as well.
Reference Tuning pitch-shifts local output to a selected reference frequency, such as retuning A=440 content to A=432. In the app, use Playback > Options > Reference Tuning for Off, 432 Hz, 440 Hz, or custom source/target Hz. It applies to local files and HTTP streams from Plex, Subsonic/Navidrome, Jellyfin, Emby, and internet radio. It is unavailable while casting because Sonos, Chromecast, and DLNA renderers receive the media URL directly.
CLI overrides are session-only:
nullplayer --cli --source local --artist "Rush" --tuning 432
nullplayer --cli --source plex --library AD-FLAC --artist "Soundgarden" --tuning 432 --tuning-source 440
nullplayer --cli --source radio --station "Radio Paradise: Mellow Mix" --tuning-offset-cents -31.766
| Key | Action |
|---|---|
Space | Pause/Resume |
q | Quit |
> / < | Next / Previous track |
→ / ← | Seek forward / backward 10s |
↑ / ↓ | Volume up / down |
s | Toggle shuffle |
r | Cycle repeat (off → all → one) |
m | Toggle mute |
i | Show track info |
For video casting, Space pauses/resumes the cast, → / ← seek on the cast session, and q stops casting before exiting. Track navigation, shuffle, repeat, mute, and volume controls are audio-only.
During music playback the CLI shows album art in the terminal. The render mode is auto-detected from the terminal's color support and can be forced:
--color-art: force color art--ascii-art: force the monochrome character-ramp — use this if your terminal reports color but renders the art as flat blocks--no-art: disable album artnullplayer --cli --source local --artist "Rush" --ascii-art
If a terminal misreports its color support (some shell profiles export COLORTERM=truecolor globally, making every terminal claim color it can't paint), set a per-terminal default in that terminal's shell profile instead of passing a flag each time:
export NULLPLAYER_ART=ascii # or: color, auto (default). Flags still override.
Framework log output is suppressed by default so the session stays clean. Pass --verbose to keep it for debugging:
nullplayer --cli --source plex --playlist "All Music" --cast "Living Room" --cast-type sonos --verbose
See nullplayer --cli --help for the full flag reference.
See AGENTS.md for documentation links and key source files.
Note: This project will never support Spotify, Youtube, Apple or Amazon. Please do not submit PRs for this type of integration.
NullPlayer does not collect or transmit personal data to the developer. Playback and usage history is stored only in the app's local SQLite database. See the Privacy Policy for details about local storage and user-directed network features.
This project is open source. Because it bundles GPL-licensed components (aubio and the PeppyMeter meter templates), the combined application is distributed under the terms of the GNU GPL v3.0 only. (The video engine is VLCKit/libVLC, which is LGPL — see below.)
The NullPlayer name, logo, icon, and other brand identifiers are not licensed for use by modified distributions. Forks, derivative works, and redistributed builds must use a different application name and replace or remove NullPlayer branding from user-facing product names, bundle names, bundle identifiers, executable names, icons, and public marketing materials unless they have prior written permission. Accurate attribution such as "based on NullPlayer" is allowed when it does not imply endorsement.
The full text of every third-party notice ships inside the app bundle at
Contents/Resources/ThirdPartyLicenses/ (aggregated in ThirdPartyNotices.txt,
with the individual license texts alongside it). scripts/build_dmg.sh runs
scripts/validate_notices.sh to fail the release if any bundled dependency is
missing its notice. See docs/third-party-notices.md
for the refresh process and scripts/third_party_components.tsv for the
authoritative component/version/license list.
Bundled third-party components:
Swift packages (compiled into the binary)
.wsz/.nps/.wmz extractionBundled frameworks / dynamic libraries
Native visualization ports (compiled into the binary)
Fonts & assets
31 followers · starred Apr 2026
Swift
89.5%
C++
6.9%
Shell
1.3%