// AUDIO_HAXOR — WALKTHROUGH

Tutorial index Docs hub
Progress
10 / 19

10.Floating player & transport

Waveform seek, three-band EQ, gain, pan, mono, speed, reverse playback, loop modes, A/B loop, shuffle, drag-reorderable history, and an autoplay chain that survives EOFs even when the WebView is throttled.

Floating player docked at the right of the app with waveform, transport, EQ, and recently-played list
assets/player.png · Floating player

Where the player lives

The floating player is not a tab — it's a draggable, collapsible HUD panel owned by frontend/js/audio.js (7 600+ lines). It's where every preview-audio action ends up. Show or hide it with P. Collapse it to a compact pill with E (expand/collapse). Close it entirely with the × button (npCloseBtn). Drag its title bar to reposition; drag the resize handle to resize. Both position and size persist in prefs.

The player can also snap to a dock — drag it near any of the four corners and it locks to dock-tl / dock-tr / dock-bl / dock-br (playerDock pref). On every successful snap the app emits a toast.player_docked notification. Each of the player's interior sections (transport, waveform, EQ, history, …) is itself drag-reorderable — the order persists as playerSectionOrder and drops emit toast.reordered_np_sections.

Transport

  • Play / pausenpBtnPlay. Shortcut Space. Function toggleAudioPlayback() (~line 2845).
  • Previous / nextnpBtnPrev / npBtnNext. Shortcuts Cmd+← / Cmd+→. Functions prevTrack(opts) / nextTrack(opts). The floating buttons always walk the player history list; shortcuts/tray/menu use { respectAutoplaySource: true } so the order follows autoplayNextSource (either the player history or the visible Samples table).
  • MutenpBtnMute. Shortcut M. Persisted in prefs (audioMuted).
  • VolumenpVolume slider, 0–100. Value displayed in npVolumePct. Pref audioVolume. Shortcut Cmd+↑ / Cmd+↓ nudges ±5%.
  • SpeednpSpeed select. Preset values: 0.25×, 0.5×, 0.75×, 1×, 1.25×, 1.5×, 2×. In reverse-playback mode the effective range widens to 0.0625–16×. Function setPlaybackSpeed(value) (~line 3579).
  • ReversenpBtnReverse and npEqBtnReverse. Decodes the full file to an AudioBuffer, reverses the samples via reverseAudioBufferAsync() (yields every 250k samples so long tracks don't stall the UI), and plays the reversed buffer through the same EQ chain.

Waveform scrubber

The waveform canvas (npWaveformCanvas) is both a visualization and a seek surface. Pointer-down anywhere on the waveform seeks. The progress overlay (npProgress) fills as playback advances; the cursor (npCursor) marks the playhead; the time display (npTime) shows elapsed / total. The canvas is fed by the same decoded-peaks cache used by the Samples table expanded rows.

Loop modes

  • Track loopnpBtnLoop, shortcut L. toggleAudioLoop() (~line 2881) flips the audioLooping state, persists to pref audioLoop, and also syncs the flag to the AudioEngine subprocess via syncEnginePlaybackLoop(audioLooping) for engine-mode playback.
  • A/B loopnpAbA sets the loop start at the current playhead, npAbB sets the end, npAbClear clears it. Shortcut B. Start/end markers npAbMarkerA / npAbMarkerB render inside the waveform. State is held in _abLoop = { start, end }.
  • ShufflenpBtnShuffle, shortcut S. Pref shuffleMode. When on, getAutoplayNextPathAfter() picks a random next path from the current autoplay source.

3-band parametric EQ

Open the EQ section with Q (toggleEqSection(), ~line 998). The EQ chain is a cascade of Web Audio BiquadFilter nodes built inside ensureAudioGraph(). Three bands:

  • LownpEqLow slider, range −12 dB to +12 dB, center ≈200 Hz, label npEqLowVal.
  • MidnpEqMid slider, range −12 dB to +12 dB, center ≈1 kHz, label npEqMidVal.
  • HighnpEqHigh slider, range −12 dB to +12 dB, center ≈8 kHz, label npEqHighVal.

The EQ canvas (npEqCanvas) plots the combined frequency response in real time so you can see the curve you've sculpted. Drag the canvas resize handle (npEqCanvasResizeHandle) to grow or shrink the plot. EQ gains restore from prefs when a new track loads.

Preamp gain, pan, mono

  • Preamp gainnpGainSlider, 0 – 2 (0%–200%), label npGainVal. Function setPreampGain(value) (~line 964). Pref preampGain. Routed through a Web Audio GainNode.
  • PannpPanSlider, −1 to +1. Label shows L, C, R with a percentage. Routed through a StereoPannerNode. Pref audioPan.
  • MononpBtnMono (M). Shortcut U. Implementation sets pan to 0 rather than doing a full ChannelMerger split so the CPU cost is trivial.

All three settings also push their values into the AudioEngine subprocess (playback_set_dsp) when engine-mode playback is active, so the engine path applies the same DSP as Web Audio preview.

Favorite / tag / note the current track

  • Favorite current tracknpBtnFav toggles the playing file in the favorites list (see step 12).
  • Tag / note current tracknpBtnTag opens the tag-and-note editor for the playing file. updateNoteBtn() (~line 4590) keeps the button's active state in sync with whether the track currently has a note or tags.

History list & filter

Below the transport lives the recently-played list (npHistoryList), built by getPlayerHistoryListItems() from the in-memory recentlyPlayed array. The array is hydrated from prefs on startup and capped by the Max recently played setting (20–500, default 50).

The list is drag-reorderable — reorder tracks to change playback order. Drops emit a toast.reordered_recently_played toast. Above it is a search input (npSearchInput) wired through the unified filter framework (registerFilter('filterNowPlaying', …)) — type to filter history + live sample search. Double-click any row to jump to that track.

The command palette (step 09) ships ready-made player track actions: Player → Play current, Player → Previous / Next, Player → Toggle loop / mute / shuffle, Player → Reverse current, Player → Show / hide / expand / collapse. Use these as a fallback when the player isn't focused (e.g. when the visualizer is active).

Autoplay chain

When the current track hits EOF, the 'ended' listener calls nextTrack({ autoplay: true }). Advance is gated on canAutoplayAdvanceTrack() — pref autoplayNext must be on. getAutoplayNextSource() chooses between player history and samples table based on pref autoplayNextSource.

If a preview fails (bad file, decode error), tryPreviewAutoplayNextOnFailureAsync(failedPath) chains forward along the same source — you can walk through a directory of corrupt files and the player will skip past them until it finds something playable.

When the WebView is hidden/minimized/unfocused, macOS throttles timers, which breaks the 'ended' event. Rust side compensates with an EOF watchdog (audio_engine_eof_watchdog_start) that polls playback status at 1 Hz and emits audio-engine-playback-eof; the JS listener forwards that to handleEnginePlaybackEofFromPoll() so the chain keeps advancing even with the app minimized.

On top of the EOF watchdog, the player also pins itself against macOS's App Nap: when an idle background app gets paged out, the first sample after a long idle would otherwise lag for seconds. A NSProcessInfo activity assertion holds the host unthrottled (the same option set Music.app uses), the audio-engine subprocess gets a 30 s keep-alive ping, and the playhead/cursor render loop drives a parallel setInterval at ~100 ms so the cursor doesn't freeze even when WKWebView throttles requestAnimationFrame to ~1 Hz on focus loss.

Engine mode vs Web Audio mode

Short previews use the browser's Web Audio pipeline (audioPlayer = HTMLAudioElement piped into the EQ chain). Longer or engine-routed playback uses the AudioEngine subprocess — the same DSP controls (gain, pan, EQ, speed, loop, reverse) apply either way because the JS layer mirrors every change into both code paths. See step 16 for the Audio Engine tab's direct controls.

Clear / export / import recently played

  • Clear history — shortcut Cmd+H.
  • Export history — from the player's settings menu, exports the recentlyPlayed array.
  • Import history — restore from a previously exported file.
TipThe EQ, gain, pan, speed, and reverse state travel with the player across tracks. Set up a preset once and every subsequent preview inherits your tone, until you explicitly reset.