A Windows desktop audio player written in C# (WinForms, .NET Framework 4.7.2) with a ten-band graphic equalizer.
| Feature | Where |
|---|---|
| Load MP3 file or files | File ▸ Add Files… (Ctrl+O) — multi-select |
| Load a folder recursively | File ▸ Add Folder… (Ctrl+Shift+O) — includes every subfolder |
| Save as playlist | File ▸ Save Playlist As… (Ctrl+S) — M3U8, M3U or PLS |
| Equalizer | Tools ▸ Equalizer… (Ctrl+E) — 10 bands, preamp, 17 presets |
Everything else below came along for the ride to make it a usable player.
Requirements: Windows, and .NET Framework 4.7.2 or later (present on every supported Windows 10 and 11). To build you need either Visual Studio Build Tools or the .NET SDK — the app itself needs neither.
No .NET SDK required. build.ps1 compiles with the Roslyn compiler bundled with
Visual Studio Build Tools, against the .NET Framework assemblies already on the
machine, using the libraries vendored in lib\.
build.cmdbuild.cmd runIf you do have the .NET SDK, MP3Player.csproj works too and restores its
dependencies from NuGet instead:
dotnet build -c ReleaseEither way the result lands in dist\Release\MP3Player.exe. Keep the executable,
the *.dll files and MP3Player.exe.config together when copying it elsewhere —
the config file carries an assembly binding redirect without which MP3 fallback
decoding fails.
dist\ is not committed; build output belongs in a
Release rather than in the history.
build-installer.ps1Produces dist\MP3Player-Setup.exe — a single ~390 KB file with the application
inside it as a compressed payload. There is nothing to download and no installer
toolchain to install: it is compiled with the same Roslyn compiler as the app.
It installs per user, into %LOCALAPPDATA%\Programs\MP3Player, which is the
decision the rest follows from — no administrator rights, no UAC prompt, and an
uninstall entry the user owns. A machine-wide install would need elevation for no
benefit in a single-user desktop player.
| Option | Default |
|---|---|
| Start Menu shortcut | on |
| Desktop shortcut | off |
| Show in "Open with" for audio files | off |
| Run when setup finishes | on |
It registers in Add/Remove Programs, copies itself in as uninstall.exe, and
offers to keep or delete your settings when removed.
MP3Player-Setup.exe /silentMP3Player-Setup.exe /uninstallTo check the installer end to end — install, launch, verify, uninstall, and confirm nothing is left behind:
build-installer.ps1 -TestNote that the installer is unsigned, so Windows SmartScreen will warn the first time it runs. Signing needs a code-signing certificate, which is a purchase rather than a build step.
Playback
- Play / pause / stop, previous / next, seek bar, volume with mute
- Shuffle (a shuffled order, so every track plays once before any repeats)
- Repeat off / all / one
- Media keys, and a full set of keyboard shortcuts (
F1lists them) - Auto-advances at the end of a track and skips files it cannot decode
Search
- Search bar directly above the playlist, filtering as you type
- Partial, case-insensitive match against song title or artist
- Multiple words narrow further —
beatles yellowfinds Yellow Submarine - Untagged tracks match on their file name, which is what the list shows
Ctrl+Fto focus,Escapeto clear- Filtering is a view: playback still runs through the whole playlist
Queue
Add to Queue(F4) andPlay Next(Shift+F4), orCtrl+Q/Ctrl+Shift+Q- Queued tracks show their position as a badge in the number column
- The queue takes priority over shuffle and normal order
- Survives sorting and removal — entries follow their tracks
Listening profile
- Favourites: click the star, or
Ctrl+D - Records play counts, completions and skips per track
- Derives genre and artist preference, shown in
Tools ▸ Listening Profile - Smart shuffle weights random selection by that profile
Show Only Favourites(Ctrl+Shift+F); history can be reset, keeping favourites- Saved to
%APPDATA%\MP3Player\profile.xml
Drive search
File ▸ Search Drives for Music…sweeps whole drives- Pick drives and file types; skips Windows and Program Files by default
- Leaves out clips shorter than a length you choose, 45 seconds by default, so a sweep does not fill the playlist with notification sounds and game audio
- Adds only files not already in the playlist
- Runs on a worker thread and can be stopped at any point
Tag editing
Edit Track Info(F2) changes title, artist, album, year, genre and track number- Writes ID3v2.3 in UTF-16, so any script works
- Preserves every frame it does not manage, including embedded cover art
- Edits several tracks at once: only the boxes you change are written
- Handles read-only files by lifting the flag for the write and restoring it
Playlist
- Virtualised list — tested with a real 7,167-track library
- Reads ID3v2.2 / 2.3 / 2.4 and ID3v1 tags, and shows embedded cover art
- Sort by title, artist, album, duration or path
- Drag and drop files, folders or playlists onto the window
- Remove selected, remove missing files, clear
- Session playlist is restored on the next launch
Equalizer
- 10 ISO octave bands: 31 Hz … 16 kHz, ±12 dB each
- Preamp, ±12 dB, for headroom when boosting
- 17 built-in presets, plus your own saved presets
- A live response curve computed from the actual filter coefficients
- Changes apply to playing audio immediately
Display
- Dark theme throughout, including menus, list and scrollbar
- FFT spectrum analyser with falling peak caps, and stereo level meters
- High-DPI aware
Formats
- Audio: MP3, WAV, WMA, AIFF, M4A, AAC, FLAC
- Playlists: M3U, M3U8, PLS (read and write)
Searching "mariah" across a 7,167-track library: it matches the artist field in every collaboration spelling, and also catches a track whose artist is Boyz II Men but whose title mentions her.
Preference is shown as lift, not a raw play count: 1.0x means a genre was played exactly as often as its share of the library predicts, so a bar past the marker is a real preference rather than a reflection of what the library happens to contain.
The harnesses in tests\ run end to end against real MP3 files — real decoding,
the real filter chain, and the real sound card — because that is where the
behaviour that matters lives.
powershell -ExecutionPolicy Bypass -File tests\run-tests.ps1 -MusicFolder "C:\Path\To\Music"PipelineTest covers 98 checks: recursive scanning, MP3 decoding, that the
equalizer measurably raises and lowers energy in the bands it should, that a flat
equalizer is bit-transparent, filter response maths, M3U/M3U8/PLS round-trips
including relative paths, shuffle and repeat semantics, the search matching rule,
the queue rules, the listening-profile maths, which keys belong to a focused text
field, ID3 tag writing, graceful handling of corrupt and missing files, and live
playback through the output device.
ShortFilterTest checks the drive-search length filter against both short clips
and real songs, confirming it catches the former without touching the latter.
ArtworkTest edits a real tagged file and checks byte-for-byte that its embedded
cover art survived.
DurationTest compares the fast header-based duration against full decoding
across a real library and reports the speed-up.
| Key | Action |
|---|---|
Space |
Play / pause |
Ctrl+← / Ctrl+→ |
Previous / next track |
Shift+← / Shift+→ |
Seek 5 seconds |
Ctrl+↑ / Ctrl+↓ |
Volume |
Ctrl+M |
Mute |
Enter |
Play the selected track |
Delete |
Remove selected tracks |
Ctrl+O / Ctrl+Shift+O |
Add files / add folder |
Ctrl+L / Ctrl+S |
Open / save playlist |
Ctrl+E |
Equalizer |
Ctrl+A |
Select all |
Ctrl+F |
Search by title or artist |
Escape |
Clear the search |
F4 / Shift+F4 |
Add to queue / play next |
Ctrl+Q / Ctrl+Shift+Q |
Add to queue / play next (alternative) |
Ctrl+D |
Toggle favourite |
Ctrl+P |
Listening profile |
Ctrl+Shift+F |
Show only favourites |
Ctrl+H / Ctrl+Shift+H |
Shuffle / smart shuffle |
F2 |
Edit track info |
F1 |
Shortcut list |
file ─▶ AudioSource ─▶ EqualizerSampleProvider ─▶ SpectrumSampleProvider ─▶ VolumeSampleProvider ─▶ WaveOutEvent
decode to 10 biquads per channel taps audio for the master volume WinMM output
float + soft limiter display
Volume sits after the equalizer, so turning it down does not change how hard the filters are driven. The analyser sits before volume, so the bars keep moving when you turn the sound down.
Each band is a peaking biquad built from the RBJ Audio EQ Cookbook formulas
(src/Dsp/BiQuad.cs), run in series, with independent filter state per channel.
- Coefficients are published to the audio thread as one immutable array by reference assignment, so the UI can move a slider without locking the audio path.
- Bands at 0 dB collapse to an identity filter and are skipped, so a flat equalizer is effectively free — and verifiably bit-transparent.
- Boosting several bands can exceed full scale, so a soft knee above 0.9 rounds peaks off instead of letting them clip hard.
- The response curve in the equalizer window is the real combined magnitude response, evaluated from the coefficients on the unit circle. It is not a decorative spline through the slider positions.
MP3 playback does not depend on Windows having an MP3 codec.
Windows "N" editions, and any install missing the Media Feature Pack, ship no MP3
decoder at all — no ACM codec and no Media Foundation. NAudio's usual
AudioFileReader throws on every MP3 on such a machine. The development machine
for this project was exactly that case, which is how it was found.
src/Audio/AudioSource.cs therefore tries the system decoder first and falls back
to NLayer, a fully managed MP3 decoder, when
none is present. The status bar shows which one is in use. The player works on any
Windows machine with nothing extra installed.
WMA, M4A, AAC and FLAC still need Media Foundation; without it the player reports a clear message naming the Media Feature Pack rather than failing silently.
Getting a track's length by opening a decoder means reading every byte of the file. Across thousands of tracks that is minutes of solid disk activity.
src/Model/Mp3Duration.cs reads the headers instead: a Xing, Info or VBRI header
gives an exact frame count, and otherwise the bitrate is sampled at intervals
across the file and applied to the audio byte count. Measured against full decoding
on 118 files from a real library, it agreed on every one and ran 15× faster.
Tags and durations load on a background thread and stream into the list in batches, so tracks appear immediately and fill in their details as the scan catches up.
The playlist is a virtual ListView, so a row number is only the same thing as a
playlist position while nothing is filtered. Once a search is active the form keeps
a list of the playlist indices being shown, and every path that crosses between the
two — playing a row, removing a selection, scrolling the current track into view,
drawing a row — goes through ViewToPlaylist / PlaylistToView rather than using
an index directly. Getting this wrong is how a filtered player ends up playing the
wrong song.
The number column keeps showing each track's real position in the playlist, not the row number, so it stays meaningful while filtered.
The matching rule lives in src/Model/TrackFilter.cs, away from the form, so it can
be tested on its own. Artist is read raw rather than through DisplayArtist,
because matching the "Unknown artist" placeholder shown in the list would make a
search for "unknown" return every untagged track.
Play counts alone describe the library, not the listener: playing forty pop
tracks means nothing if the library is mostly pop. UserProfile therefore scores
each genre and artist by lift — plays received against plays predicted from its
share of the library — and smooths that towards neutral with a prior, so a
two-track genre with one play cannot outrank a hundred-track genre.
Plays are weighted by how much of the track was actually heard. Reaching the end counts in a track's favour, abandoning it in the first third counts against it, and anything in between is left uncounted rather than guessed at.
Smart shuffle multiplies those signals together — favourite, familiarity, skip rate, genre and artist lift, and how recently the track was played — then picks at random in proportion. The group terms are square-rooted and the final weight clamped, so a favoured genre nudges the odds instead of monopolising them, and nothing in the library ever becomes unreachable.
A tag writer that emits only the fields shown in its editor quietly destroys
whatever else the file carried: cover art, lyrics, ReplayGain. Id3Writer reads
the existing frames, replaces the ones it manages, and writes the rest back
verbatim. It converts ID3v2.2 picture frames to the v2.3 layout rather than
copying a payload that would be malformed.
The new tag and the untouched audio are written to a temporary file which then replaces the original, so an interruption cannot leave a corrupt song. Read-only files — which most ripped and copied libraries are — have the flag lifted for the write and restored afterwards.
WinForms processes menu shortcuts in ProcessCmdKey, which runs before the
focused control sees the key. Any shortcut that collides with an ordinary editing
key therefore steals it. With Delete bound to "Remove Selected", pressing Delete
to clear the search box removed tracks from the playlist; with Space bound to
play/pause, typing a space started playback.
TextEntryKeys.BelongsToTextField decides which keys a focused text field owns —
deleting, typing, caret movement, selection and the clipboard set — and
ProcessCmdKey hands those straight through before any shortcut, its own or the
menu's. Everything else, including F2 and F4, stays an application shortcut.
Whether a text field has the caret is read from the search box's own focus events
rather than from Form.ActiveControl, which reports the custom wrapper rather
than the TextBox nested inside it.
ListView does not double buffer, so an owner-drawn dark list flickers whenever it
repaints — very visible while thousands of tags stream in. PlaylistListView turns
on both Control.DoubleBuffered and the native LVS_EX_DOUBLEBUFFER, paints the
erase pass in the theme colour, and repaints only the rows actually on screen, at a
capped rate.
It also asks the native control which rows are selected via LVM_GETITEMSTATE.
In virtual mode the State on DrawListViewItemEventArgs is unreliable and can
report every row as selected.
Directory.GetFiles(path, "*", SearchOption.AllDirectories) aborts the whole
enumeration the first time it meets a folder it cannot read. MediaScanner walks
an explicit stack instead, skipping unreadable folders, stepping over reparse
points so junction loops cannot trap it, and reporting progress as it goes. It runs
on a worker thread and can be cancelled.
MP3Player/
├── build.cmd, build.ps1 Build without an SDK
├── build-installer.ps1 Package MP3Player-Setup.exe
├── installer/ Installer sources (payload embedded at build time)
├── MP3Player.csproj/.sln Optional, for Visual Studio / dotnet build
├── assets/app.config Binding redirect + high-DPI settings
├── lib/ NAudio + NLayer assemblies
├── dist/Release/ Build output, ready to run
└── src/
├── Program.cs Entry point, crash log
├── MainForm.cs Main window
├── AppSettings.cs Settings, saved to %APPDATA%\MP3Player
├── Audio/
│ ├── AudioPlayer.cs Playback, signal chain
│ └── AudioSource.cs Decoder selection + managed MP3 fallback
├── Dsp/
│ ├── BiQuad.cs RBJ peaking filter, response evaluation
│ ├── Equalizer.cs 10-band EQ, soft limiter, peak metering
│ ├── EqualizerPresets.cs 17 presets
│ └── SpectrumAnalyzer.cs FFT tap for the display
├── Model/
│ ├── Track.cs Track model
│ ├── TrackFilter.cs Search matching rule
│ ├── TrackStats.cs Per-track play history
│ ├── UserProfile.cs Favourites, affinity, smart-shuffle weights
│ ├── Id3Writer.cs ID3v2.3 writing, preserving unknown frames
│ ├── Id3Reader.cs ID3v1 / ID3v2 tags and cover art
│ ├── Mp3Duration.cs Fast header-based duration
│ ├── MediaScanner.cs Recursive folder scan
│ ├── MetadataLoader.cs Background tag/duration worker
│ ├── Playlist.cs Ordering, shuffle, repeat
│ └── PlaylistFile.cs M3U / M3U8 / PLS read and write
└── Ui/ Theme, search box, key routing and custom controls
Settings, the restored session playlist and any crash log live in
%APPDATA%\MP3Player.
The equalizer, spectrum analyser, ID3 reader, duration reader, playlist handling and all UI controls are part of this project.
Copyright (c) 2026 Rex Collao Ballester rexballester@gmail.com
MIT — see LICENSE.
The player redistributes the NAudio and NLayer assemblies in lib\, both MIT
licensed; their notices are in THIRD-PARTY-NOTICES.md.
Everything else here — the equalizer and its filters, the spectrum analyser, the
ID3 reader and writer, the duration reader, playlist handling, the listening
profile and every UI control — is original to this project.





