Skip to content

WinMusic v1.0.1

Latest

Choose a tag to compare

@Patrickjaillet Patrickjaillet released this 01 Aug 22:54
· 1 commit to main since this release

Changelog

All notable changes to WinMusic are documented in this file.
The format follows Keep a Changelog,
and this project adheres to Semantic Versioning.

[1.0.1] - 2026-08-02

Fixed

  • scripts/build_installer.ps1: auto-detects ISCC.exe (Inno Setup
    compiler) instead of assuming it's on PATH. Checks, in order: the
    current PATH, the default Inno Setup 7 (64-bit) install location
    (C:\Program Files\Inno Setup 7\), then the default Inno Setup 6
    (32-bit) location (C:\Program Files (x86)\Inno Setup 6\). Raises a
    clear, actionable error (download link + -IsccPath override
    instructions) instead of PowerShell's raw "term not recognized" error
    when Inno Setup isn't found anywhere.

[1.0.0] - 2026-08-02

First stable release. Feature set is now frozen; the 1.0.x line will
only receive bug fixes.

Added

  • scripts/generate_screenshot.py: renders MainWindow off-screen
    against a small in-memory demo library to produce a genuine, reviewable
    assets/docs/screenshot.png for the README (not part of the shipped
    application).

Fixed

  • LibraryFilterProxyModel: replaced the deprecated
    QSortFilterProxyModel.invalidateFilter() calls with the
    beginFilterChange()/endFilterChange() pair recommended since Qt
    6.6, removing the last remaining deprecation warnings from the test
    suite.
  • tests/test_audio_analysis.py: two tests had gone stale after the
    PcmCapture crash-safety fix (introduced while packaging for v0.9.5)
    that defers wiring libVLC's audio_set_format/audio_set_callbacks
    from __init__ to an explicit attach() call. The tests still
    asserted the pre-fix, immediate-registration behaviour and were
    failing under a real libVLC install. Rewritten to reflect the current,
    correct contract (attach() registers the callbacks exactly once,
    idempotently, and only once called) instead of reverting the
    underlying safety fix.
  • Minor style debt cleared project-wide (black, ruff): three
    over-length lines reflowed (core/database/dao.py,
    ui/views/playlist_panel.py), one unused import removed, import
    ordering fixed, and justified noqa annotations added for the Qt
    model API overrides that cannot follow Python naming/default-argument
    conventions (rowCount, columnCount, headerData in
    LibraryTableModel). Zero outstanding black/ruff findings.

Changed

  • Application version bumped to 1.0.0 (main.py, pyproject.toml,
    installer/winmusic.iss).
  • README.md rewritten for the stable release: drops the "planned
    for v1.0.0" framing, documents the actual installer download/install
    steps, and embeds a real interface screenshot instead of the previous
    placeholder note.
  • Full regression pass: pytest + pytest-qt, 144 tests passing, 1
    skipped (a crossfade-start test that requires a live OpenGL context,
    unavailable in headless/offscreen environments — unrelated to this
    release).

[0.9.5] - 2026-08-01

Added

  • PyInstaller --onedir packaging (winmusic.spec, project root):
    freezes src/main.py into a windowed (console=False) WinMusic.exe
    plus its supporting dist/WinMusic/ folder, using the app icon
    (assets/icons/winmusic.ico). Bundles the shaders/ folder and
    assets/icons/ as data, and explicit hiddenimports for the runtime
    dependencies PyInstaller's static analysis cannot always discover on
    its own (vlc, pytesseract, imagehash, acoustid, PIL).
    Optionally vendors the native runtimes the app needs at runtime
    (libVLC, Tesseract OCR, Chromaprint fpcalc) from a local vendor/
    staging folder (see vendor/README.md) so the packaged app stays
    Offline-First: any vendor sub-folder missing at build time is skipped
    rather than failing the build, and the matching optional feature
    degrades gracefully at runtime, exactly as already implemented in
    core/cover_recognition/ocr.py and core/fingerprinting/.
  • core/app_paths.py: centralizes resolution of bundled-resource
    directories (shaders_dir(), icons_dir(), vendor_dir()) so the
    same code works both from a source checkout and from a frozen
    PyInstaller --onedir build (is_frozen() / sys.executable-relative
    resolution instead of a __file__-relative one, which is not reliable
    once modules run from PyInstaller's frozen archive).
    ui/visualizer/shader_registry.py and main.py now resolve the
    shaders folder and the app icon through this module instead of ad hoc
    Path(__file__).resolve().parents[...] expressions.
  • Inno Setup installer script (installer/winmusic.iss): builds
    WinMusicSetup-<version>.exe from the PyInstaller --onedir output,
    with a Start Menu group + uninstaller shortcut, an optional desktop
    shortcut, an optional (opt-in, unchecked by default) .mp3 file
    association, a clean uninstall ([UninstallDelete] removes the
    install directory only — the local database/logs/settings under
    %AppData%\WinMusic are preserved across reinstalls), 64-bit-only
    install (ArchitecturesAllowed=x64compatible), and
    MinVersion=10.0 to enforce the Windows 10/11 target.
  • scripts/build_installer.ps1: orchestrates the full release build
    on a Windows machine — dependency install, pyinstaller winmusic.spec --clean --noconfirm, then ISCC.exe installer\winmusic.iss — failing
    fast with a clear error if either step doesn't produce its expected
    output.
  • vendor/README.md: documents the exact folder layout
    (vendor/vlc/, vendor/tesseract/, vendor/chromaprint/fpcalc.exe)
    and upstream sources expected before running the packaging build, and
    how to verify a produced dist/WinMusic/ folder on a clean Windows
    10/11 VM with no Python/VLC/Tesseract installed system-wide.
  • Test suite extended with test_v095_features.py (frozen vs.
    source-checkout path resolution in core.app_paths, PyInstaller spec
    content — onedir/console/icon/datas/hidden imports, Inno Setup script
    content — required sections, shortcuts, opt-in .mp3 association,
    clean-uninstall behaviour, build script wiring, and cross-file version
    consistency between main.py/pyproject.toml/winmusic.iss) — 14 new
    tests, 143 tests cumulated (VLC-backed playback tests and the
    OpenGL-backed visualizer tests remain environment-dependent on a real
    libVLC install / GPU context, unrelated to this release).

Changed

  • Application version bumped to 0.9.5 (main.py, pyproject.toml,
    installer/winmusic.iss).
  • .gitignore: excludes vendored native runtime binaries
    (vendor/*, keeping vendor/README.md tracked) in addition to the
    existing *.spec/build//dist/ exclusions — none of this packaging
    build output or third-party binary is published to the source-free
    public repository (§9 of ROADMAP.md).

[0.9.0] - 2026-08-01

Added

  • About tab (ui/about_tab.pyAboutTab widget + AboutDialog):
    displays application name, version, copyright, e-mail and website (as
    clickable links), and a short license summary. Reachable from a new
    Help > About WinMusic… menu entry.
  • Application and installer icons (assets/icons/winmusic.ico,
    assets/icons/installer.ico): multi-resolution .ico files (16, 24,
    32, 48, 64, 128, 256 px) generated from a single dark-themed,
    accent-purple design (scripts/generate_icons.py, Pillow-based, fully
    offline/reproducible). The app icon is applied to the QApplication
    window icon at startup.
  • Persisted user settings (core/database/dao.pySettingsDAO
    backed by the existing app_settings table): generic
    get/set/get_int/set_int/delete/all key-value accessors with
    INSERT ... ON CONFLICT DO UPDATE upsert semantics. Three settings are
    now persisted and restored across sessions: last scanned library
    folder (library_folder, pre-fills the folder picker), last shader
    used in the visualizer (last_shader_id, restored on startup), and
    playback volume (volume).
  • Local error logging (core/logging_setup.py): rotating local log
    file (<app-data>/logs/winmusic.log, 2 MB × 3 backups, UTF-8),
    offline-only (no network handler is ever attached), plus a
    sys.excepthook that logs any unhandled exception before the default
    handler runs. Initialized at startup in main.py. Previously-silent
    except: pass blocks in MainWindow (stored cover read failures,
    perceptual-hash computation failures) now log a warning instead of
    failing silently, and scan/tag-write failures are logged with
    logger.exception/logger.error in addition to the existing UI
    message boxes.
  • Test suite extended with test_v090_features.py (SettingsDAO
    CRUD/upsert/int-coercion, configure_logging/log file creation and
    content, About tab constants) — 11 new tests, 129 tests cumulated
    (VLC-backed playback tests remain environment-dependent on a real
    libVLC install, unrelated to this release).

Changed

  • MainWindow: restores persisted settings after the initial UI/library
    load (_restore_settings), and now persists the shader selection and
    volume as they change, and the scanned folder each time File > Scan Folder… is used.
  • main.py: application version bumped to 0.9.0; local logging is
    configured before the Qt application starts.

[0.8.0] - 2026-08-01

Added

  • Online enrichment (core/online_enrichment/), manual/on-demand only,
    never called automatically in the background (Offline-First):
    • MusicBrainzClient: recording search (artist + title) and free-text
      release search (used to search from OCR output), both against the
      public MusicBrainz web service (JSON API, proper User-Agent).
    • CoverArtClient: downloads the front cover for a MusicBrainz release
      id from the Cover Art Archive; raises a dedicated CoverArtError
      (including a clean 404 → "no cover available" case) instead of
      crashing the UI.
    • genre_fallback.py: full ID3v1 genre code table (0-79) used to keep
      genre resolution functional even fully offline / when MusicBrainz has
      no usable genre relation.
  • Cover recognition (core/cover_recognition/), fully offline:
    • image_utils.py: normalizes a pasted/imported screenshot (RGB
      conversion, max-dimension downscaling) before OCR/hashing.
    • ocr.py: local text extraction via pytesseract, degrading
      gracefully (OcrUnavailableError) instead of crashing when the local
      Tesseract engine is missing/broken.
    • perceptual_hash.py: pHash computation and Hamming-distance matching
      (imagehash) with a configurable confidence threshold.
    • recognizer.py (CoverRecognizer): orchestrates OCR + perceptual
      matching against the covers already known in the local library —
      entirely offline, no network call is ever made from this module.
    • storage.py: local on-disk storage for recognized/downloaded covers
      (<app-data>/covers/album_<id>.<ext>).
  • Cover Recognition screen (ui/dialogs/cover_recognition_dialog.py):
    import a screenshot from a file or the clipboard, see the local OCR
    text (editable), review ranked offline matches against the library,
    optionally trigger an online MusicBrainz search (explicit button, never
    automatic) with a live cover-art preview from the Cover Art Archive,
    and confirm via a dedicated "Use This Match" step — nothing is written
    to the database or to ID3 tags until the user explicitly confirms.
    Reachable from the library's right-click menu ("Identify Cover Art…").
  • AlbumDAO: set_cover_hash, set_musicbrainz_id, get_by_id,
    list_with_cover_hash, list_all. ArtistDAO.set_musicbrainz_id.
    TrackDAO.get() (raw entity lookup, used to preserve the existing
    genre when cover recognition updates a track's artist/album/year).
  • id3_writer.embed_cover(): writes/replaces the front-cover APIC
    frame on a track's ID3 tags (used after a confirmed cover match).
  • Database migration v2: adds albums.cover_hash (+ index) so
    perceptual-hash matching against the library works fully offline.
  • Test suite extended with test_online_enrichment.py (MusicBrainz /
    Cover Art Archive clients, mocked HTTP, no real network calls) and
    test_cover_recognition.py / test_v080_features.py (OCR, pHash,
    recognizer, DAO roundtrips, embed_cover) — 109 tests passing (VLC
    playback tests remain environment-dependent on a real libVLC install,
    unrelated to this release).

Changed

  • LibraryView gained a cover_recognition_requested signal and an
    "Identify Cover Art…" entry in the track context menu.
  • MainWindow now owns app_data_dir (derived from the SQLite database
    path) to resolve where recognized/downloaded covers are stored locally.

[0.7.0] - 2026-08-01

Added

  • Final 5 audio-reactive GLSL shaders (src/shaders/*.frag, GLSL 330 core,
    same "Image"-pass-only convention as the first 5), completing the full
    set of 10 required by the roadmap: Liquid Metal Bass Ripple (bump-mapped
    specular ripple driven by bass), Starfield Treble Sparkle (warp-speed
    starfield with treble-driven twinkle), Voronoi Audio Morph (animated
    Voronoi cells reacting to mid/onset), Fractal Julia Audio Zoom (Julia set
    fractal whose c parameter and zoom are modulated by bass/mid/onset,
    capped at 48 iterations for performance), Waveform Ribbon 3D Illusion
    (5 layered sine ribbons with a pseudo-3D depth/perspective illusion).
  • ShaderVisualizerWidget (ui/visualizer/shader_widget.py) now renders
    through an off-screen QOpenGLFramebufferObject sized at a configurable
    render_scale (default 0.75, clamped 0.51.0 via
    set_render_scale()) before presenting the result upscaled to the
    widget's full resolution through a dedicated present pass. This cuts the
    number of fragment-shader invocations roughly quadratically with the
    scale factor, targeted at integrated/laptop GPUs, while keeping visual
    density (bar counts, cell counts, etc.) unaffected since none of the
    shaders hard-code pixel counts.
  • Smooth crossfade transitions between shaders: set_shader() now keeps
    the outgoing shader's program alive and renders both the outgoing and
    incoming shader into separate off-screen framebuffers for
    TRANSITION_DURATION_MS (550 ms), blending them via a small present
    shader (u_tex_from / u_tex_to / u_mix) instead of hard-cutting on
    shader switch, whether triggered from the panel selector or
    programmatically.
  • Test suite extended (shader registry/GLSL validation tests now cover
    all 10 shaders instead of 5, plus new coverage for render_scale
    clamping and for the crossfade transition state machine) — 79 tests
    cumulated, 100% passing (the crossfade test is skipped, rather than
    failed, in environments without a real OpenGL context, e.g. headless
    CI without a GPU/X server).

Changed

  • shader_registry.SHADER_REGISTRY now lists all 10 shaders; the shader
    selector in VisualizerPanel and the full-screen visualizer therefore
    expose all 10 without any UI code changes, since both already iterate
    list_shaders().

Added

  • ShaderVisualizerWidget (ui/visualizer/shader_widget.py): a
    QOpenGLWidget-based renderer targeting OpenGL 3.3 core. Compiles a
    shared full-screen-triangle vertex shader against a loadable GLSL
    fragment shader (.frag), driven by a dedicated 60 FPS (~16 ms)
    QTimer render loop that pauses automatically while the widget is
    hidden. Falls back to a safe solid-color program and emits
    shader_compile_failed if a shader fails to compile/link, instead of
    crashing the UI.
  • Shader registry (ui/visualizer/shader_registry.py): a Qt-independent
    module listing the available shaders, resolving their file paths under
    src/shaders/, and validating/loading their GLSL source.
  • First 5 audio-reactive GLSL shaders (src/shaders/*.frag, GLSL 330
    core, Shadertoy-style "Image" pass only, no iChannel), all driven by
    the u_bass / u_mid / u_treble / u_amplitude / u_onset /
    u_onset_strength / u_time / u_resolution uniforms fed from the
    v0.5.0 UniformBus: Spectrum Bars Radial, Plasma Waves, Tunnel Warp,
    Particle Field Pulse, Kaleidoscope Frequency.
  • VisualizerPanel (ui/views/visualizer_panel.py): shader selector
    (QComboBox) plus a live embedded preview, added to MainWindow
    between the library/playlist splitter and the player bar.
  • Full-screen visualizer mode (FullscreenVisualizerWindow): opened via
    the panel's "Fullscreen" button or the new Visualizer → Fullscreen
    menu action (F11), closes on Esc.
  • Test suite extended (15 new tests: shader registry listing/lookup,
    GLSL source validation for all 5 shaders — required uniforms present,
    valid #version/main() — widget shader switching, panel selector
    wiring) — 71 tests cumulated, 100% passing.

Fixed

  • PcmCapture.__init__ (core/audio_analysis/pcm_capture.py): current
    python-vlc releases require the opaque parameter to be passed
    explicitly to MediaPlayer.audio_set_callbacks() (it has no default
    value); the call was missing it, raising TypeError: ... missing 1 required positional argument: 'opaque' at startup on real Windows
    installs.
  • PcmCapture: the play callback given to audio_set_callbacks() must be
    a ctypes function pointer (CFUNCTYPE(None, c_void_p, c_void_p, c_uint, c_int64)), not a plain bound method; libVLC rejected it with
    ctypes.ArgumentError: ... 'method' object cannot be interpreted as ctypes.c_void_p. The callback is now wrapped accordingly and the
    reference kept alive on the instance.
  • PcmCapture._play_callback: samples (the pointer to the raw audio
    buffer) and count (the per-channel sample count) were swapped when
    computing the byte length and reading from memory, which would have
    crashed or read garbage as soon as real playback reached the callback
    (previously untested, since prior tests only exercised the callback
    indirectly via feed_raw()).
  • Test stub (FakeVlcPlayer in tests/test_audio_analysis.py) updated
    to match the real 6-argument audio_set_callbacks signature, and a new
    test now drives the callback through the real ctypes function pointer
    (as libVLC would) to cover this class of regression going forward.

[0.5.0] - 2026-08-01

Added

  • Real-time PCM capture (core/audio_analysis/pcm_capture.py): hooks into
    libVLC's audio callback API (audio_set_format / audio_set_callbacks)
    to tap the currently-playing PCM stream directly — no microphone or
    WASAPI loopback device required. AudioPlayer.enable_pcm_capture()
    wires this transparently onto the existing playback engine.
  • FFT band extraction (core/audio_analysis/fft_analyzer.py): windowed
    real FFT (scipy.fft.rfft) extracting bass (20–250 Hz), mid
    (250–4000 Hz) and treble (4000–20000 Hz) band energies plus overall RMS
    amplitude from each analysis frame.
  • Onset (beat) detection with temporal smoothing
    (core/audio_analysis/onset_detector.py): spectral-flux based detector
    with an adaptive threshold (rolling mean + sensitivity × standard
    deviation) and a minimum inter-onset interval to avoid duplicate
    triggers on sustained transients.
  • Uniforms bus (core/audio_analysis/uniform_bus.py): thread-safe,
    EMA-smoothed, clamped-to-[0,1] snapshot (u_bass, u_mid, u_treble,
    u_amplitude, u_onset, u_onset_strength, u_time) ready for
    consumption by the shader rendering engine (v0.6.0+).
  • AudioAnalysisEngine (core/audio_analysis/engine.py): orchestrates
    capture → FFT → onset detection → uniforms bus on each tick(); wired
    into MainWindow on a dedicated 60 FPS (~16 ms) timer, active only
    while a track is playing.
  • Test suite extended (18 new tests: PCM capture/downmix, FFT band
    discrimination, onset detection/suppression, uniform bus
    smoothing/clamping, end-to-end engine tick) — 56 tests cumulated, 100%
    passing.

[0.4.0] - 2026-08-01

Added

  • Manual metadata editing (EditMetadataDialog): form to edit title, artist,
    album, genre, track number and year; writes both the SQLite record
    (TrackDAO.update_metadata) and the file's ID3 tags via the new
    core/metadata/id3_writer.py (mutagen-based, TIT2/TPE1/TALB/TCON/TRCK/TYER).
  • Custom genre management (GenreManagerDialog + GenreDAO.rename/delete):
    create, rename and delete genres from the Library menu; deleting a genre
    unlinks it from tracks instead of deleting them.
  • Playlists (PlaylistDAO, PlaylistPanel): create, rename, delete;
    add/remove tracks; drag-and-drop reordering (reorder); export to and
    import from M3U files (core/library/playlist_io.py).
  • Rating system (1–5 stars) via right-click context menu on the library
    table (LibraryView context menu: Play, Edit Metadata, Add to Playlist,
    Set Rating, Delete from Library); ratings persisted through
    TrackDAO.set_rating (clamped to 0–5). Play count already incremented by
    PlayerController since v0.2.0 and now surfaced/edit-able through the
    same table.
  • TrackDAO.get_with_details, TrackDAO.delete,
    PlaylistDAO.tracks_with_details and related joined-query helpers.
  • Main window now uses a splitter: library table on the left, playlist
    panel on the right.
  • Test suite extended (10 new tests: ID3 write, genre CRUD, track metadata
    update/rating, playlist CRUD/reorder, M3U export/import) — 38 tests
    cumulated, 100% passing.

[0.3.0] - 2026-08-01

Added

  • Main application window (MainWindow, PySide6 QMainWindow): library view
    on top, persistent player bar at the bottom, File menu (Scan Folder,
    Refresh Library, Exit), status bar for scan feedback.
  • Single frozen dark theme (ui/themes/dark_theme.py): fixed color palette,
    QPalette + global stylesheet applied via Fusion style — no light/dark
    switch, per project conventions (§8 of ROADMAP).
  • Library table view (LibraryView, LibraryTableModel,
    LibraryFilterProxyModel): sortable columns (title, artist, album, genre,
    year, duration, bitrate, rating, play count) via clickable headers, backed
    by TrackDAO.list_with_details() (SQL join across artists/albums/genres).
  • Instant search bar filtering across title/artist/album/genre as the user
    types, combined with dropdown filters for genre, artist and year
    (LibraryFilterProxyModel).
  • Player bar (PlayerBar): transport controls (previous/play-pause/next),
    volume slider, seek slider with live position/duration labels, wired to
    PlayerController.
  • TrackDAO.list_with_details(): joined query returning display-ready rows
    for the library table.
  • Folder scanning triggered from the UI (File > Scan Folder…), reusing
    LibraryScanner, with result/error feedback in the status bar.
  • Test suite extended (8 new tests) covering the table model and the
    filter/sort proxy (search, genre/artist/year filters, combined filters) —
    37 tests cumulated, 100% passing.

[0.2.0] - 2026-08-01

Added

  • AudioPlayer: low-latency playback engine wrapping python-vlc
    (load, play, pause, resume, toggle_pause, stop, seek by ms or ratio,
    volume get/set, state, end/error event hooks).
  • PlaybackQueue: playback queue with add/remove/clear and next/previous
    navigation.
  • PlaybackHistory: bounded in-memory playback history.
  • PlayerController: orchestrates the player, the queue and the history;
    increments play_count in the database on playback and automatically
    advances to the next queued track when one ends.
  • Test suite extended to 29 tests, including real headless playback tests
    against libVLC (dummy audio/video output) and controller integration
    tests.

Fixed

  • Potential libVLC deadlock: control calls (play, set_media, ...)
    triggered from within a MediaPlayerEndReached/MediaPlayerEncounteredError
    callback are now dispatched on a dedicated thread instead of running
    directly on libVLC's internal event thread.
  • Database connections are now safe to use from the audio callback
    thread (check_same_thread=False + internal lock serializing all
    statements).

[0.1.0] - 2026-08-01

Added

  • Project scaffolding (src/, pyproject.toml, black/ruff configuration).
  • SQLite database layer with a versioned migration system (core/database).
  • Initial schema: artists, albums, genres, tracks, playlists,
    playlist_tracks, app_settings.
  • Data models: Artist, Album, Genre, Track, Playlist.
  • Data-access layer (DAO) for artists, albums, genres and tracks with
    idempotent get-or-create semantics.
  • ID3 tag reader based on mutagen (title, artist, album, genre, track
    number, year, duration, bitrate).
  • Recursive local-folder scanner that indexes .mp3 files into the
    database, updates existing entries, and removes entries for files that
    no longer exist on disk.
  • Unit test suite (pytest) covering the database, the DAO layer and the
    scanner (11 tests).