Skip to content

Repository files navigation

MP3 Player

A Windows desktop audio player written in C# (WinForms, .NET Framework 4.7.2) with a ten-band graphic equalizer.

Main window

The four requested features

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.

Building and running

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.cmd
build.cmd run

If you do have the .NET SDK, MP3Player.csproj works too and restores its dependencies from NuGet instead:

dotnet build -c Release

Either 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.

Installer

build-installer.ps1

Produces 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 /silent
MP3Player-Setup.exe /uninstall

To check the installer end to end — install, launch, verify, uninstall, and confirm nothing is left behind:

build-installer.ps1 -Test

Note 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.

Features

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 (F1 lists 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 yellow finds Yellow Submarine
  • Untagged tracks match on their file name, which is what the list shows
  • Ctrl+F to focus, Escape to clear
  • Filtering is a view: playback still runs through the whole playlist

Queue

  • Add to Queue (F4) and Play Next (Shift+F4), or Ctrl+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)

Search

Search

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.

Listening profile

Listening profile

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.

Drive search and tag editing

Equalizer window

Equalizer

Tests

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.

Keyboard shortcuts

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

How it works

Signal chain

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.

The equalizer

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 decoding without a system codec

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.

Reading a large library quickly

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.

Searching a virtual list

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.

Measuring preference

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.

Editing tags without losing anything

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.

Shortcuts versus the text caret

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.

Keeping the list smooth

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.

Folder scanning

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.

Layout

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.

Third-party components

  • NAudio 2.2.1 — audio input/output (MIT)
  • NLayer 2.0.1 — managed MP3 decoder (MIT)

The equalizer, spectrum analyser, ID3 reader, duration reader, playlist handling and all UI controls are part of this project.

Licence

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.

About

A Windows desktop audio player in C# (WinForms, .NET Framework 4.7.2) with a ten-band graphic equalizer.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages