Skip to content

v0.12.0

Choose a tag to compare

@github-actions github-actions released this 30 Apr 20:44
· 19 commits to main since this release
b7e9577

Added

  • MIDI controller configuration in web UI: The controllers section now supports full
    editing of MIDI controller event mappings (play, prev, next, stop, all_songs, playlist)
    with optional section_ack and stop_section_loop events. Includes an "Add MIDI" button
    alongside existing gRPC and OSC controller options.

  • Reusable MIDI event editor component: Extracted a shared MidiEventEditor component
    used by both the MIDI controller configuration and the song MIDI event editor. Displays
    the event type dropdown and contextual parameter fields in a compact horizontal layout.

  • Song exclude MIDI channels: The MIDI tab in the song editor now shows a 16-channel
    toggle grid when a MIDI file is configured, allowing users to exclude specific channels
    from playback. Commonly used to skip drums (channel 10) or lighting data channels.

  • Notification audio configuration in web UI: New Notifications tab in the hardware
    profile editor allows configuring custom audio files for loop armed, break requested,
    loop exited, and section entering events, plus per-section-name overrides. Includes
    filesystem browse and drag-and-drop upload.

  • Per-song notification overrides: New Notifications tab in the song editor allows
    overriding profile-level notification sounds for individual songs. Per-section override
    names autocomplete from the song's defined sections.

  • Global max sample voices setting: The samples section in the config editor now
    includes a max sample voices field (default 32) controlling the global polyphony limit.

  • Status events in hardware profile: Status events (MIDI events emitted on player
    state changes for off/idling/playing) are now configured per-profile in the new Status
    Events tab. Each state supports a list of MIDI events. Legacy top-level status_events
    is automatically normalized into the profile.

  • MIDI-to-DMX editor in web UI: The MIDI section now has a full editor for MIDI-to-DMX
    passthrough mappings, replacing the previous read-only note. Each mapping configures a
    MIDI channel routed to a DMX universe, with optional Note Mapper and CC Mapper
    transformers that remap note/CC numbers before output.

Improved

  • Web UI: Nonchord brand redesign: A second, brand-led redesign pass on top of the design
    system overhaul, restyling the entire UI to the Nonchord visual language (warm-paper light
    theme, Nunito display + Inter body + JetBrains Mono technical, pink and cyan identity
    accents) while keeping the existing information architecture and gRPC/WebSocket plumbing.

    Visual treatment:

    • New design tokens (--nc-*) for color, type scale, spacing, shadows, and motion. Legacy
      tokens are remapped semantically so existing components inherit the new palette without
      churn.
    • Nunito display font for titles, Inter for body, JetBrains Mono for technical readouts
      (loaded from Google Fonts with preconnect).
    • Hero PlaybackCard with hard-shadow "pop" treatment, pixel-eq corner accent, 48px circular
      play button (pink while playing, cyan when stopped), and 8px scrub with section regions.
    • PlaylistCard, TracksCard, LogsCard, EffectsCard, StageView all rebuilt with overline +
      Nunito title heads, --card-border separators, and the Nonchord badge taxonomy.
    • Songs browser with magnifier-prefixed search, group cards with overline labels, song rows
      with MIDI/LIGHT/DMX badges, hover-reveal delete, and a phone collapse to name + badges +
      chevron.
    • Songs detail with cyan back-link + chevron, Nunito 32 title, dirty-dot tab bar, MIDI
      channel grid in cyan-tinted 8-up, ink-on-inset YAML editor.
    • Playlists, Config, Status, and Lighting editor pages restyled to match the same panel-card
      chrome, cyan-inset selection states, and Nunito heading hierarchy.
    • Lighting timeline cue blocks tinted cyan with a cyan-500 selection ring; lane labels and
      toolbar promoted to the design tokens; tempo, ruler, and stage simulator canvases now
      branch on theme so they read clearly on both light and dark backgrounds.
  • Live-show telemetry & live-mode safety: A series of UX-Findings improvements turning
    the redesign's chrome into something that actually behaves like a live tool.

    • Lock now disables editing. Save / delete / destructive buttons across PlaylistEditor,
      ConfigEditor, SongList, SongDetail, and LightingEditor become disabled while the
      player is locked (with a tooltip explaining why). A thin amber LIVE — locked stripe
      surfaces under the topnav, and unlocking from the topnav requires a confirm dialog
      ("Unlock during a live session?"). Locking is still one click.
    • Real dirty tracking with an in-app navigation guard. Save buttons render as ghost +
      disabled when clean, primary + enabled when dirty, with an "Unsaved" pill next to Save.
      A new lib/dirtyGuard.ts lets editors register their dirty state with the router; the
      hashchange listener prompts to discard unsaved changes when the user navigates to a
      different page-level scope. Tab nav within the same editor (e.g. Songs detail tab
      switches) intentionally does not prompt — same scope, same component, edits persist.
      beforeunload handlers stay as a backstop for tab close / refresh.
    • Connection dot reflects subsystem health. New lib/ws/status.ts polls /api/status
      every 5s while the WebSocket is up and exposes a derived health store (ok / warn / error
      / unknown). The topnav .topnav__conn becomes a link to #/status whose color reflects
      worst-case health: green when all required subsystems are connected, amber on
      initializing or controllers-in-error, red when audio (always required) or any configured
      MIDI / DMX subsystem is not connected.
    • Topnav playhead progress bar. A 2px pink fill at the bottom edge of the topnav
      reflects elapsed/total while playing, so users can tell where in the song they are
      without looking at the dashboard.
    • ERROR / WARN log row tinting. ERROR rows in the dashboard logs panel get a pink-
      tinted row background and a bold left edge stripe; WARN rows get the same treatment in
      amber. Impossible to miss while scanning during a show.
    • Status page deep-links. Each subsystem row that isn't currently connected gets a
      cyan Configure → or Fix → pill that deep-links to the active profile's section
      in the config editor (audio / midi / lighting / trigger; DMX routed to the lighting
      section).
    • Currently-playing indicator on the Songs browser. The row whose name matches the
      active song gets a pink left-edge stripe and a Playing or Loaded pill — mirrors the
      playlist-row treatment on the dashboard.
  • Web UI: a11y and phone polish:

    • NavDrawer is a real modal. Tab / Shift-Tab cycle focus within the open drawer,
      aria-modal is set, and focus is restored to whatever opened the drawer (typically the
      hamburger button) on close. The role and aria-modal are only set while the drawer is
      actually open so global [role="dialog"] queries don't match the hidden drawer.
    • Cyan / pink contrast on text. Promote color: var(--nc-cyan-500) → cyan-600 (and
      pink-500 → pink-600) wherever those colors appear as text on the warm-paper background.
      Brand wordmark, drawer brand, .mono--cyan / .mono--pink utilities, unmapped-track
      label, back-link hover. Dark-mode overrides (cyan-300 / pink-300) unchanged.
    • Phone tab scroll affordance. Right-edge gradient mask on the song-detail tab bar at
      ≤720px so users can see there's more to scroll.
    • Lock toggle on the mini-player. Move the lock toggle from the drawer footer to the
      mini-player on phone — the mini-player is the live-show object and is already pinned
      to the bottom.
    • MIDI exclude-channels presets. Add None / All / Drums only preset chips above
      the 16-channel exclude grid in song detail. "Drums only" matches the most common
      live-show preset (mtrack runs the drum channel, everything else is played live).
    • Phone read-only lighting summary. New LightingSummary component shows tempo,
      show + cue counts, sequence + cue counts, and the distinct effect types used in the
      song. Both Songs detail's Lighting tab and the standalone LightingEditor page render
      this on phone-sized viewports instead of the unusable timeline editor.
  • User-facing light/dark theme toggle: The redesign already supported .nc--dark on
    <html> but had no UI to flip it. The topnav now has a sun / moon / half-disc button that
    cycles system → light → dark → system, with the choice persisted to
    localStorage["mtrack-theme"]. A tiny inline IIFE in index.html applies the chosen
    theme before first paint to avoid a flash of the wrong palette.

  • Phone-only chrome: 280px slide-in NavDrawer (with backdrop, focus trap, Esc to close)
    replaces the dropdown hamburger menu, and a sticky bottom MiniPlayer (transport + lock
    toggle + song title that jumps to the dashboard) is always visible on phone-sized
    viewports.

  • Web UI design system and accessibility overhaul: Comprehensive design review and
    redesign pass across all pages, establishing a stronger design language and improving
    accessibility for assistive technologies.

    Design system foundations:

    • Added type scale tokens (--text-xs through --text-xl) replacing 7+ ad-hoc font sizes.
    • Added semantic color tokens (--accent-subtle, --red-subtle, --yellow-subtle,
      --blue-subtle, --green-subtle) — hardcoded rgba() values throughout components now
      reference design tokens.
    • Added --bg-surface token (was referenced but undefined).
    • Added .sr-only utility class, .badge global component, .checkbox-row form utility,
      and shared form layout classes (.section-fields, .field, .field-row, .field-header).
    • Added prefers-reduced-motion media query respecting user motion preferences.

    Accessibility (WCAG compliance):

    • Replaced all: unset on playlist buttons with explicit resets so focus indicators work.
    • Added aria-expanded to hamburger menu and sample collapsible headers.
    • Added aria-pressed to log level filter toggles, role="log" to log container.
    • Added role="img" and aria-label to canvas waveforms.
    • Added role="tabpanel" with aria-labelledby to all tab content panels.
    • Fixed song delete button from inaccessible <span tabindex="-1"> to proper <button>.
    • Fixed song row from nested <button> (invalid HTML) to <div role="link">.
    • Fixed sample header from suppressed-a11y div to role="button" with keyboard support.
    • Added keyboard focus handlers to tooltips (onfocus/onblur).
    • Added WAI-ARIA arrow-key navigation to profile editor tab bar.
    • Added aria-label to status page subsystem dots.
    • Scoped SectionBar keyboard handler to focused container — prevents Delete key from
      destroying sections while typing in other inputs.

    Visual polish:

    • Replaced all emoji/Unicode icons in nav (play/pause, lock/unlock) with inline SVGs.
    • Thickened playback progress bar from 6px to 10px for better touch targets.
    • Improved loop badge sizing and error message treatment (8s timeout, dismiss button,
      colored background).
    • Added left-border accent to current playlist song for clearer visual indication.
    • Added music icon to dashboard empty state.
    • Added dirty indicator (* in yellow) to profile editor title when unsaved.
    • Improved cue block visibility in timeline (raised base opacity, stronger hover).
    • Widened cue color strip from 3px to 4px.
    • Added pulsing animation to disconnected status indicator.
    • Added tab overflow gradient fade on profile editor tab bar.

    Layout optimization:

    • Dashboard card-pair uses flexible height (min-height/max-height) instead of rigid 280px.
    • Effects card uses flexible width instead of fixed 280px.
    • Status page widened from 700px to 1000px with 2-column grid layout.
    • Timeline lane labels widened from 80px to 100px across all lane types.
    • Timeline bottom panel is now collapsible with toggle button.

    UX improvements:

    • Fixed playlist drag-and-drop with stable slot IDs (was using fragile song + i key).
    • Waveform canvas now applies DPR scaling for crisp rendering on HiDPI/Retina displays.
    • Transport uses CSS Grid layout with section controls spanning full width.
    • New sequence cue references auto-select the first available sequence definition.
  • Web UI UX overhaul: Comprehensive usability pass across all pages, focused on
    reducing clicks, preventing data loss, and improving visual consistency.

    Data loss prevention:

    • Playlist editor warns before switching playlists or leaving the page with unsaved changes.
    • Sample deletion now requires confirmation.
    • "Remove section" confirmation in profile editor describes what will be lost (e.g.,
      "This will delete 5 track mappings and all audio settings.").
    • Ctrl+S / Cmd+S keyboard shortcut for saving in the config editor.

    Live performance usability:

    • Dashboard playlist songs are now clickable to jump directly to a song during playback.
    • Full-width disconnection banner when WebSocket connection drops.
    • Improved text contrast for dim/muted text (WCAG AA compliant).
    • Hardcoded English strings in playback card moved to i18n.

    User flow improvements:

    • Song detail tabs reduced from 7 to 5: MIDI merged into Tracks, Notifications merged
      into Config (both as collapsible sections).
    • File browser now starts in the song's directory instead of filesystem root.
    • "Import from Filesystem" is now the primary button in the song list; "New Song" is secondary.
    • Song list search query persists when navigating back from a song detail view.

    Visual design consistency:

    • Added missing CSS variables (--bg-hover, --bg-danger, --text-danger, z-index tokens).
    • Extracted shared .error-banner, .panel, .panel-header, .btn-icon classes from
      duplicated component styles into global app.css.
    • Standardized border-radius across all cards and panels.
    • Unified z-index layering with CSS variable tokens.

    Polish:

    • Status page auto-refreshes every 5 seconds with "Updated Xs ago" indicator; build info
      moved to the bottom; distinguishes "Not Configured" from "Not Connected."
    • Playlist editor shows position numbers, drag feedback, and filters out the all_songs
      system playlist.
    • Dashboard shows a consolidated empty state with action links when no playlist is loaded;
      hides empty cards (tracks, effects, stage view) to reduce noise.
    • Log card has level filter pills (TRACE/DEBUG/INFO/WARN/ERROR), defaulting to INFO+.
    • Loading spinners replace plain "Loading..." text across all pages.
    • Sample rename is now discoverable via a pencil icon (not just double-click).
    • Channel mapping inputs validate and show inline errors for non-numeric values.
    • Controller "Add" buttons have descriptive tooltips explaining each type.
    • Device refresh buttons show loading state during enumeration.
    • NotFound page has a "Back to Dashboard" button.
    • Lock button tooltip explains the consequence of locking/unlocking.
    • Stage view fixture drag positions persist to localStorage across page reloads.
    • Nav bar song name truncation relaxed (300px desktop, 150px mobile); mobile nav shows
      current page name next to the brand.
    • Tab hover states have subtle background highlight.
    • Aria-labels added to all icon-only buttons for screen reader accessibility.

Fixed

  • Missing OSC path overrides: Added section_ack, stop_section_loop, and loop_section
    to the OSC controller advanced path overrides panel.

  • Flaky save test: Fixed race condition in song config save test by replacing a
    synchronous boolean flag with waitForRequest.

  • Config editor URL rewrite on refresh: Navigating to a deep-linked profile tab URL
    (e.g. #/config/profile-name/midi) no longer rewrites to the bare profile URL on load,
    so refreshing the page preserves the active tab.

  • Song looping with crossfade: Songs can now be configured to loop indefinitely by
    setting loop_playback: true in song.yaml. Audio crossfades seamlessly at loop boundaries
    (100ms linear fade), MIDI restarts from the beginning, and the lighting/DMX timeline resets
    cleanly. During a looping song, pressing Play or Next breaks out of the loop, advances the
    playlist, and auto-plays the next song. Stop cancels everything as usual.

  • Beat grid detection from click tracks: Audio click tracks (tracks named "click") are
    analyzed offline to detect beat positions and measure boundaries. The result is a BeatGrid
    with absolute beat times and accented-beat indices, stored in a per-song disk cache
    (.mtrack-cache.json) alongside waveform peaks. Accent classification uses a pluggable
    AccentClassifier trait — the default ZcrClassifier separates click sounds by
    zero-crossing rate (timbral differences), with AmplitudeClassifier as an alternative.
    Beat grid data is exposed via gRPC proto and displayed in the web UI (measure/beat position
    during playback, beat/measure counts in song detail).

  • Song analysis disk cache: Computed song data (waveform peaks, beat grids) is now persisted
    to .mtrack-cache.json in each song's directory. The cache uses file mtime+size for
    invalidation — if an audio file changes, its cached data is recomputed on next access.
    This eliminates redundant waveform computation on restarts.

  • Audio crossfade primitives: New CrossfadeCurve enum (Linear, EqualPower) and
    GainEnvelope struct for applying time-varying gain to audio sources. The mixer's
    ActiveSource now supports an optional gain envelope, applied per-block during mixing.
    Sources with a completed fade-out envelope are automatically finished. These primitives
    support both song looping and future song-to-song crossfade transitions.

  • Morningstar SysEx integration: mtrack can now automatically push the current song name
    to a Morningstar MIDI controller (MC3, MC6, MC8, MC6 Pro, MC8 Pro, MC4 Pro) via SysEx
    when songs change. Configured via an optional morningstar block on the MIDI controller,
    this eliminates the need for hand-maintained per-song program change mappings. Supports
    short/long preset names, configurable preset slots, save-to-flash, and custom model IDs.

  • Morningstar configuration in web UI: The MIDI controller section in the hardware profile
    editor now includes a Morningstar preset naming panel with model selection, preset number,
    name type, and save-to-flash options.

  • Song change notifier system: New SongChangeNotifier trait on the player enables
    pluggable reactions to song changes. The Morningstar integration is the first consumer;
    the trait is generic and supports multiple concurrent notifiers.

  • Section looping: Named sections of a song (defined by measure ranges in song.yaml)
    can be activated during playback via gRPC LoopSection/StopSectionLoop RPCs. Audio
    crossfades at section boundaries (same 100ms linear fade as whole-song looping), MIDI
    restarts from section start with hard cut, and DMX/lighting timelines reset to the
    section's start time. Elapsed time reporting accounts for accumulated loop iterations
    via a loop_time_consumed accumulator. Section activation is rejected if playback has
    already passed the section end. A confirmation tone (1kHz, 50ms, -12dB sine with fade
    envelope) plays through the mtrack:looping track mapping when a section loop activates.

  • Visual section editor: The Sections tab in the song detail view now shows a
    canvas-based timeline with all track waveforms, beat grid measure lines, and interactive
    section creation/editing. Sections can be created by dragging on empty space (snaps to
    measure boundaries), resized by dragging edges, moved by dragging the body, renamed by
    double-clicking, and deleted with the Delete key. Measure label density and snap
    granularity adapt to zoom level using power-of-2 stride thinning. Zoom controls include
    +/-, Fit, and Ctrl+scroll wheel with anchor-point zooming.

  • Section loop UI controls: The PlaybackCard shows section buttons when playing a song
    with defined sections. Clicking a section button activates the loop; an active loop shows
    the section name and a "Stop Loop" button. Next/Prev navigation is allowed during looping.

  • Section config validation: Song validation now checks section constraints (name not
    empty, start_measure >= 1, end_measure > start_measure).

  • Visual lighting timeline: duration-based blocks: Effect blocks in the visual editor
    now display their actual duration as block width (previously all blocks were a fixed 500ms
    width). A right-edge drag handle allows resizing effects directly on the timeline, which
    updates the effect's duration parameter. New effects created via double-click default to
    duration: 5s.

  • Visual lighting timeline: per-layer lanes: The single "effects" lane is replaced by
    three layer lanes — Foreground, Midground, Background — each showing only effects assigned
    to that layer. The show name appears in its own header row above the lanes. Layer lanes are
    derived from the LAYERS array for future configurability.

  • Visual lighting timeline: sequence expansion: Sequence references are now expanded
    inline into the layer lanes, showing each iteration's effects at their correct timeline
    positions. Sequence-originated blocks are visually distinct with dashed borders and a pink
    tint. The sequences lane shows sequence references as blocks spanning their full expanded
    duration (all loop iterations), and dragging the right edge adjusts the loop count, snapping
    to whole iterations.

  • Tempo map detection from MIDI: When a song has a MIDI file, the lighting
    tempo editor can extract an authoritative tempo map directly from MIDI
    SetTempo and TimeSignature meta events. Consecutive monotonic BPM changes
    (ritardandos/accelerandos) are automatically collapsed into single transitions.
    Falls back to beat grid estimation when no MIDI file is available. The beat
    grid's start offset (first audible beat) is used in both cases.

  • Tempo map estimation from beat grid: Songs with a click track but no MIDI
    file can estimate a tempo map from the detected beat grid. Finds stable tempo
    sections, detects time signature changes from measure boundary spacing, and
    identifies discrete tempo changes. Results are snapped to measure boundaries.
    Displayed with an "estimated from beat grid" badge to indicate it's a guess.

  • Beat grid refinement: Beat positions within stable tempo sections are
    snapped to an ideal grid after onset detection, removing ±5-15ms jitter from
    the onset detector. This improves tempo calculation accuracy at section
    boundaries.

  • Tempo editor in visual timeline: The tempo map editor is now accessible
    by clicking the tempo lane in the visual timeline. Includes controls for BPM,
    time signature, start offset, and tempo changes. A "Detect from MIDI" or
    "Guess from beat grid" button populates the tempo map automatically.

  • Snap subdivisions: The snap resolution in both the main timeline and the
    sequence editor now includes 1/2, 1/4, 1/8, and 1/16 beat subdivisions in
    addition to beat and measure snapping.

  • MIDI alignment quality warning: After detecting a tempo map from MIDI,
    the lighting editor computes a beat-alignment RMSE between MIDI-predicted
    beat positions and click-track detections. If the error exceeds 15 ms, a
    warning badge is shown indicating the MIDI file may not match the recording.
    The alignment_rms_ms field is included in the GuessedTempo API response.

  • Compound meter beat stepping: Tempo detection from MIDI now correctly
    handles 6/8, 9/8, and 12/8 time signatures by stepping by dotted-quarter
    note pulses (three eighth notes) rather than quarter notes, matching the
    natural click-track pulse for compound meters.

  • Lighting file management in song editor: Light show files (.light) can
    now be added and removed directly from the song detail lighting tab. Each file
    is listed with a remove button; adding or removing files updates the song.yaml
    lighting: array. File creation and deletion are deferred until Save, so
    navigating away without saving leaves the disk untouched. Both operations
    respect the player lock.

  • Delete lighting file API: New DELETE /api/lighting/:name endpoint for
    removing .light files from disk, with SafePath validation and .light
    extension enforcement.

  • Implicit lighting file on editor load: Opening the lighting tab for a song
    with no .light files automatically creates an implicit file with a default
    "Main" show, so effect lanes are immediately visible. The file is only written
    to disk when the user saves.

  • Resize snap to grid: Dragging an effect's resize handle now snaps the
    duration to the nearest beat or measure boundary (matching the timeline's snap
    resolution setting). Hold Ctrl/Cmd while releasing to bypass snap for
    free-form sizing.

  • Measure-based duration output: Effect durations produced by resize and
    other UI operations now prefer measure/beat units (e.g. 1measure, 2beats)
    over time units when the duration aligns cleanly to the tempo grid. Falls back
    to ms/s for non-aligned values.

  • Double-click creates effect on layer lanes: Double-clicking on a
    foreground/midground/background lane now creates a default static effect
    assigned to the correct layer, with a 1measure duration when tempo is
    available. Previously, double-click only worked on the combined "effects" lane.

Fixed

  • Effect block width uses max duration: CueBlock width now reflects the
    maximum duration across all effects in the cue, rather than only the first
    effect's duration.
  • Effect resize was non-functional: The oneffectresize callback was never
    wired from TimelineEditor to ShowGroup, so dragging the resize handle had
    no effect. Now connected with a handler that updates all effects in the cue.
  • CuePropertiesPanel not showing for layer lanes: Clicking an effect on a
    layer lane (e.g. effects:foreground) didn't show the properties panel because
    the tab matching required an exact "effects" string. Now normalizes
    "effects:*" sub-lane types to "effects" for tab selection.
  • Tempo lost when adding light files: Setting tempo on a song with no
    existing .light files, then adding a file, would lose the tempo because it
    was only stored in the merged state and never persisted to a file. Now
    auto-creates a backing file for tempo-only changes and inherits the current
    tempo when creating new files.
  • WebSocket connection banner: The "Not connected to server" banner in the
    lighting editor used a manual store subscription pattern that could miss
    updates. Switched to the reactive $wsConnected store syntax.
  • song.yaml lighting key handling: buildYaml() now explicitly manages the
    lighting key — non-empty arrays are preserved, empty arrays clean up the key
    entirely.
  • Flaky playlist save test: The playlist mutations "save calls API" test
    checked a boolean synchronously after clicking Save, racing against the async
    fetch. Replaced with page.waitForRequest().

Changed

  • Lighting effects: explicit durations required (breaking change): The lighting engine no
    longer supports perpetual or permanent effects. Every effect must have an explicit duration
    or hold_time parameter. Effects that previously ran indefinitely until replaced (e.g.,
    static color: "red" with no duration) are no longer valid — the parser will reject them
    with a clear error message. This is a breaking change to the lighting show file format; old
    show files must be updated to include durations on all effects.

Removed

  • Effect replacement semantics: Effects no longer automatically kill conflicting effects on
    the same layer. Multiple effects can now coexist on the same layer simultaneously, with the
    blend mode determining how overlapping effects combine. This simplifies the mental model:
    effects are independent, finite blocks on a timeline.
  • Persistent fixture state: The engine no longer preserves an effect's final state after it
    completes. When an effect's duration expires, its contribution to the output is gone. Dimmer
    effects no longer persist their final brightness level. This includes removal of the
    fixture_states store, channel locking, and the is_permanent() concept.

Changed

  • Player method refactoring: Converted several static Player methods (emit_midi_event,
    prev_and_emit, next_and_emit, report_status) to instance methods, reducing parameter
    passing and simplifying call sites. Playlist navigation now uses a PlaylistDirection enum
    instead of function pointers.
  • Consistent parking_lot::Mutex usage: All std::sync::Mutex instances across the
    codebase have been converted to parking_lot::Mutex for consistency (no poisoning, simpler
    API). The only exception was already using parking_lot::Condvar.

Fixed

  • WebSocket test isolation: Playwright e2e tests now use namespace-based WebSocket routing
    (unique wsId per test) to prevent cross-test message contamination via the shared mock
    server.