Audio Analyzer Window
SkillMediaThis skill gives your AI working knowledge of the Audio Analyzer windows in the nullplayer project, both the classic and modern versions. Once added, your AI can help you build or change the analyzer and its panes, including Scope, Levels, Spectrogram, Octave, Pitch, and Delay.
Available today. Use it from your connected AI after setup.
No other account needed.
Add the skill, then tell your AI which analyzer window or pane you want to change. Describe the edit you have in mind and it will work within that part of the project.
Then ask your AI: use the Audio Analyzer Window skill
What your AI can do with it
- Modify either the classic or modern Audio Analyzer window
- Work on individual panes such as Scope, Levels, Spectrogram, Octave, Pitch, and Delay
- Update the stereo PCM path that feeds the analyzer panes
- Adjust per-pane consumer gating
- Work with the shared AudioAnalysisDSP module
What this skill tells your AI
The instructions your AI receives, as published by ad-repo/nullplayer in skills/audio-analysis-window/SKILL.md and read by ahel’s review.
A classic and modern UI window offering a Friture-style (https://friture.org) real-time view of the playing audio. Exactly one pane is visible at a time; the pane is selected via the window's right-click context menu (no in-window controls — matches the Spectrum window). Modeled on the Spectrum windows in each UI mode.
Accessing
- Window menu / main window right-click context menu → Audio Analyzer
- Available in classic and modern UI modes.
Window chrome & sizing
- Center-stack window. Registered as
CenterStackWindowKind.audioAnalysisinWindowManager. It opens at the same width as the main window, docks/snaps flush in the vertical stack and is vertically stretchable like spectrum/playlist windows.showAudioAnalysisappliesapplyCenterStackSizingConstraints/applyDefaultCenterStackFrameForCurrentHT/normalizedCenterStackRestoredFrameexactly likeshowSpectrum. The controller's default size isModernSkinElements.spectrumWindowSize. - UI Size.
WindowManager.applyDoubleSize(previousScale:)has an explicit audio-analysis block (after the waveform block) that rescales the frame by the current UI Size transition ratio. Without it the window is the one stack window that doesn't follow Medium/Large size changes. - Hide Title Bars. Included in
effectiveHideTitleBars(for:)(sub-window list), so it auto-hides when docked and follows the global HT toggle. The view readseffectiveHideTitleBarsfor its title-bar height, drawing, and hit-testing. - Dragging. The entire non-control face drags the window; see the ui-guide window-dragging requirement.
- Chrome rendering — modern.
ModernAudioAnalysisViewdraws the modern skin title bar + close button (spectrum_prefix) and insets the SwiftUI hosting view into the content area viacontentAreaRect()/layoutHostingView(). Its hosting view uses an aqua/darkAqua appearance selected from the modern skin brightness. - Chrome rendering — classic.
AudioAnalysisViewusesSkinRenderer's playlist/spectrum-style chrome with theAUDIO ANALYZERbitmap title and no shade button. It hosts the same SwiftUI pane content inside the classic side and bottom borders. Neither mode may cover its chrome with the pane's opaque black background.
Panes
| Pane | Source notification | Consumer(s) it registers | Render |
|---|---|---|---|
| Scope (oscilloscope) | .audioPCMDataUpdated (userInfo["pcm"] 512 mono) | spectrum | CoreGraphics (NSView) |
| Levels (peak/RMS) | .audioStereoPCMDataUpdated (left/right 512) | stereo | SwiftUI bars |
| Spectrogram (waterfall) | .audioSpectrumDataUpdated (spectrum 75 bands) | spectrum | Metal (MTKView) |
| Octave (1/3-octave spectrum) | .audioFFTMagnitudesUpdated (magnitudes raw linear, sampleRate, fftSize) | magnitudes | CoreGraphics (NSView) bars + peak-hold |
| Pitch (fundamental frequency) | .audioPCMDataUpdated (512 mono) | spectrum | SwiftUI (Hz, note name, cents deviation) |
| Delay (L/R phase delay) | .audioStereoPCMDataUpdated (left/right 512) | stereo | SwiftUI (ms, samples, direction) |
.audioPCMDataUpdated is posted inside the spectrum block, so the Scope and Pitch panes register a
spectrum consumer (not a dedicated PCM one) to make mono PCM flow.
Octave uses the magnitudes consumer alone. The magnitudes path is independent: the FFT runs when
spectrumNeeded || magnitudesNeeded, and the 75-band spectrum work is gated separately by
spectrumNeeded. So a magnitudes consumer triggers the FFT and .audioFFTMagnitudesUpdated on its
own — no spectrum consumer required, and an octave-only window skips the 75-band mapping. Both
AudioEngine and StreamingAudioPlayer implement this gating identically.
What each pane shows (behavior + accuracy caveats)
- Scope — time-domain oscilloscope: a green waveform line of the 512-sample mono PCM frame over
a faint grid. Each frame is trigger-aligned to a rising zero-crossing in its first quarter so
periodic content stays phase-locked (stationary) instead of swimming, then gently blended into the
previous frame (
frameWeight) to reduce flicker, and drawn as a smoothed quadratic curve. Idles flat on silence (no trigger found → no lock). - Levels — per-channel Peak and RMS vertical meters (LEFT/RIGHT), in dBFS over a −120…0 dB range. Meter color: green normally, yellow above −12 dB, red above −6 dB. RMS responds more smoothly than peak.
- Spectrogram — scrolling waterfall (Metal). Each new column is one 75-band spectrum frame mapped through a Viridis colormap (dark = quiet, bright = loud); low frequencies at the bottom, time scrolls right-to-left.
- Octave — 1/3-octave bar spectrum, 20 Hz–20 kHz, one bar per band growing up from the baseline. Bar color green → yellow (>0.4) → red (>0.7) by level; a white peak-hold tick marks each band's recent maximum and decays 85%/frame. Caveat: the 2048-pt FFT is ≈21.5 Hz/bin, so sub-200 Hz bands have sparse bin coverage and look coarse/step-like.
- Pitch — fundamental-frequency tracker (autocorrelation). Shows NOTE (nearest note + octave,
e.g.
A4) and FREQ (Hz), or—when there is no confident pitch (silence, noise, or polyphonic material). A cents bar shows deviation from equal temperament: green in tune (<5¢), yellow (<20¢), red beyond. Caveat: the 512-sample window (~11.6 ms) is reliable for vocals/treble but unreliable below ~100 Hz (octave errors). - Delay — stereo inter-channel delay (L/R cross-correlation). Shows DELAY (ms), SAMPLES, a direction string ("right lags left" / "left lags right" / "aligned"), and a needle centered at 0. Caveat: the 512-sample window resolves only ±~5.8 ms (at 44.1 kHz); larger delays alias.
These caveats are inherent to the existing 2048-pt FFT / 512-sample PCM frames the panes consume — they are display limits, not bugs. If a pane needs better low-frequency or long-delay resolution, the fix is a larger analysis buffer, not pane code.
Architecture
-
Windows:
Windows/ModernAudioAnalysis/ModernAudioAnalysisWindowController.swiftandWindows/AudioAnalysis/AudioAnalysisWindowController.swiftconform toApp/AudioAnalysisWindowProviding.swift. Their skin-specific NSViews host the shared pane content. -
Shared UI and gating:
Windows/AudioAnalysis/AudioAnalysisContentView.swiftandAudioAnalysisConsumerCoordinator.swift. -
Pane selection state:
AudioAnalysisModel: ObservableObject(@Published var selectedPane), owned by the NSView and observed byAudioAnalysisContentView. The right-clickmenu(for:)mutates it (radio items + Close); the shared content view's.onChangepersists it (UserDefaults) and invokes itsonPaneChangecallback, which each window's NSView routes tocontroller.setVisiblePane(_:)→AudioAnalysisConsumerCoordinator. -
Panes: shared files under
Windows/AudioAnalysis/:ScopePaneView.swift,LevelsPaneView.swift(vertical full-height peak/RMS meters, no title text),SpectrogramPaneView.swift,OctavePaneView.swift(1/3-octave bands CoreGraphics with peak-hold),PitchPaneView.swift(Hz + note + cents SwiftUI), andDelayPaneView.swift(stereo delay cross-correlation SwiftUI). -
Shader:
Visualization/SpectrogramShaders.metal— fullscreen quad generated from[[vertex_id]](no vertex buffer), samples anr32Floathistory texture, Viridis colormap LUT. Low frequencies at the bottom (texCoord.y = 0). Loaded viaBundleHelper.url(...)+device.makeLibrary(source:)—makeDefaultLibrary()returns nil in SPM executables (same pattern asSpectrumAnalyzerView). The.metalfile is a.copyresource inPackage.swift. -
DSP:
Sources/NullPlayerCore/Audio/AudioAnalysisDSP.swift— pure-Swift, unit-tested (Tests/NullPlayerCoreTests/AudioAnalysisDSPTests.swift):peakDBFS,rmsDBFS,octaveBands,estimatePitchHz(autocorrelation),estimateDelaySamples(L/R cross-correlation). -
Stereo path:
Audio/AudioEngine.swiftandAudio/StreamingAudioPlayer.swiftpublish.audioStereoPCMDataUpdated(downsampled L/R) gated byaddStereoConsumer/removeStereoConsumer/stereoNeeded, mirroring the spectrum/waveform consumer pattern. Wired in both the local engine and the streaming delegate (streamingPlayerDidUpdateStereoPCM(left:right:sampleRate:)). The PeppyMeter window (peppymeter) is another consumer of this same stereo path — any change to it must keep both the Levels/Delay panes and PeppyMeter working. The Cava window (cava) instead adds a sibling full-rate tap (.audioStereoPCMFullDataUpdated, undecimated 2048-sample L/R) — the audio-analysis skill's own "larger buffer, not pane code" guidance in practice — emitted from both playback paths as well. -
Magnitudes path (new):
Audio/AudioEngine.swiftandAudio/StreamingAudioPlayer.swiftpublish.audioFFTMagnitudesUpdated(userInfo:"magnitudes"raw linear half-spectrum,"sampleRate","fftSize") gated byaddMagnitudesConsumer/removeMagnitudesConsumer/magnitudesNeeded. Posted inside the spectrum FFT block, after magnitudes are computed but before dB conversion. Octave pane is the sole consumer; the gating cost is zero when no Octave pane is visible.
Consumer gating (CPU)
AudioAnalysisConsumerCoordinator owns the gating (shared by both windows): setVisiblePane(_:)
maps each pane to its consumer(s):
- Scope (pane 0) → spectrum
- Levels (pane 1) → stereo
- Spectrogram (pane 2) → spectrum
- Octave (pane 3) → magnitudes
- Pitch (pane 4) → spectrum
- Delay (pane 5) → stereo
Only the visible pane's consumer(s) are registered; others are removed. deregisterAll() removes
everything via a consumerRemovers map that tracks which remove function to call per consumer ID.
The controller registers the initial pane in showWindow(_:); the shared content view re-invokes
it on .onChange(of: model.selectedPane). Both stopRenderingForHide() and windowWillClose call
consumerCoordinator.deregisterAll() so a hidden/closed window leaves the FFT, stereo, and
magnitudes paths idle. The spectrogram also pauses its MTKView (isPaused) while hidden or
miniaturized.
Persistence
Window frame saved in AppStateManager session state (audioAnalysisWindowFrame). On restore it's
gated by a UI-mode match (modeMatches), so a frame saved in classic only restores in classic and
vice-versa. The selected pane persists via UserDefaults
(AudioAnalysisModel.selectedPaneDefaultsKey). Docks/snaps with Main/EQ/Playlist/Spectrum via
WindowManager.
Gotchas
- Notifications post from the audio thread. Observers must marshal to main: AppKit observers
use
addObserver(forName:object:queue:.main); the SwiftUI Levels pane uses.publisher(for:).receive(on: DispatchQueue.main). Mutating SwiftUI@Stateoff-main is a bug. - Spectrogram bands are already 0–1.
.audioSpectrumDataUpdated's"spectrum"is 75 bands normalized 0–1 (seeAudioEnginedoc comment), NOT dBFS. Feed them straight to the colormap. Treating them as dBFS (e.g.(v + 80) / 80) pushes every band to ≈1.0 → the whole window floods yellow. - Shader load (SPM): load the
.metalsource viaBundleHelper.url(...)and compile withdevice.makeLibrary(source:).device.makeDefaultLibrary()returns nil in SPM executables → "Failed to load shader library" and a blank pane. - Metal: guard
pipelineStateandhistoryTexturebefore creating the command encoder (see metal-gotchas); watch the render-to-texture y-flip. The spectrogram uses pacedMTKViewdrawing (preferredFramesPerSecond), not a manual timer loop. - Modern independence / shared layer: files under
Windows/ModernAudioAnalysis/must not import fromSkin/orWindows/MainWindow/. The shared files inWindows/AudioAnalysis/(content view, model, coordinator, panes) must stay mode-neutral — no modern- or classic-skin-specific imports; only the classic shell (AudioAnalysisView/AudioAnalysisWindowController) may useSkin/. - Two playback paths: any new tap data must be emitted by both
AudioEngineand theStreamingAudioPlayerdelegate route, or streaming silently misses it.
See visualizations for the visualization index and spectrum-analyzer-window for the related spectrum window.
Signals
- GitHub stars
- 118
- Forks
- 9
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
audio-analysis-window- Source
- github.com/ad-repo/nullplayer