Add Psytrance Visualizer macOS app with Metal rendering

A complete audio-reactive visualizer for psytrance music featuring:

Audio Analysis (DSPEngine):
- FFT spectrum analysis via Accelerate/vDSP
- 64-band Mel spectrogram
- Sub-bass energy extraction (<100Hz)
- Automatic sidechain pump detection
- Harmonic-to-Noise ratio (HNR) calculation
- Peak/transient detection

8 Visualization Modes (Metal Shaders):
1. FFT Classic - Frequency spectrum bars with glow
2. Mel Spectrogram - Waterfall display
3. Sub-Bass - Pulsating rings
4. Sidechain Pump - Breathing zoom effect
5. Harmonic/Noise - Geometric vs chaotic particles
6. Mandelbrot - Audio-reactive fractal zoom
7. Tunnel Warp - Infinite tunnel with distortion
8. DMT Geometry - Sacred geometry patterns

Features:
- Selectable audio input device (BlackHole support)
- Configurable buffer size (512/1024)
- Reactivity slider for visual intensity
- Auto-hiding control panel
- Fullscreen support with keyboard shortcuts (1-8, F, ESC)
- Persistent settings via UserDefaults
- Psytrance-inspired neon/UV color palette
This commit is contained in:
Claude
2025-12-22 21:36:45 +00:00
parent b607a9cd8a
commit a22c238dc4
29 changed files with 4780 additions and 0 deletions
@@ -0,0 +1,90 @@
//
// AppSettings.swift
// PsytranceVisualizer
//
// Persistent application settings
//
import Foundation
/// Application settings that are persisted between sessions
struct AppSettings: Codable {
/// Selected audio input device UID
var selectedAudioDeviceUID: String?
/// Audio buffer size (512 or 1024 samples)
var bufferSize: Int
/// Last used visualization mode (1-8)
var lastVisualizationMode: Int
/// Reactivity slider value (0.0 - 1.0)
var reactivity: Float
/// Whether app was in fullscreen mode
var isFullscreen: Bool
/// Last window frame (for restoration)
var windowFrame: CodableRect?
/// Volume/gain adjustment
var inputGain: Float
/// Whether to show FPS counter
var showFPS: Bool
/// Default settings
static var `default`: AppSettings {
AppSettings(
selectedAudioDeviceUID: nil,
bufferSize: 1024,
lastVisualizationMode: 1,
reactivity: 0.5,
isFullscreen: false,
windowFrame: nil,
inputGain: 1.0,
showFPS: false
)
}
/// Available buffer sizes
static let availableBufferSizes = [512, 1024]
/// Validates and clamps settings to valid ranges
mutating func validate() {
// Clamp buffer size to valid options
if !AppSettings.availableBufferSizes.contains(bufferSize) {
bufferSize = 1024
}
// Clamp visualization mode
if lastVisualizationMode < 1 || lastVisualizationMode > 8 {
lastVisualizationMode = 1
}
// Clamp reactivity
reactivity = max(0.0, min(1.0, reactivity))
// Clamp input gain
inputGain = max(0.0, min(2.0, inputGain))
}
}
/// Codable wrapper for CGRect
struct CodableRect: Codable {
var x: Double
var y: Double
var width: Double
var height: Double
init(from rect: CGRect) {
self.x = Double(rect.origin.x)
self.y = Double(rect.origin.y)
self.width = Double(rect.size.width)
self.height = Double(rect.size.height)
}
var cgRect: CGRect {
CGRect(x: x, y: y, width: width, height: height)
}
}
@@ -0,0 +1,111 @@
//
// AudioAnalysisData.swift
// PsytranceVisualizer
//
// Audio analysis data structure containing all DSP results
//
import Foundation
/// Contains all audio analysis data computed by DSPEngine
struct AudioAnalysisData {
// MARK: - FFT Data
/// Raw FFT magnitude spectrum
var fftMagnitudes: [Float]
// MARK: - Mel Spectrogram
/// 64 Mel frequency bands
var melBands: [Float]
// MARK: - Sub-Bass Analysis
/// RMS energy below 100Hz (0.0 - 1.0)
var subBassEnergy: Float
/// History buffer for time-based visualization
var subBassHistory: [Float]
// MARK: - Sidechain Detection
/// Current envelope follower value (0.0 - 1.0)
var sidechainEnvelope: Float
/// Detected pumping amount (0.0 - 1.0)
var sidechainPumpAmount: Float
/// Whether pump is currently active
var isPumping: Bool
// MARK: - Harmonic-to-Noise Ratio
/// HNR ratio (0.0 = noise, 1.0 = pure harmonic)
var hnrRatio: Float
// MARK: - Transient Detection
/// Whether a transient peak was detected
var isPeak: Bool
/// Intensity of the detected peak (0.0 - 1.0)
var peakIntensity: Float
// MARK: - Stereo Channels
/// Left channel samples
var leftChannel: [Float]
/// Right channel samples
var rightChannel: [Float]
// MARK: - Additional Analysis
/// Spectral centroid (brightness) normalized 0.0 - 1.0
var spectralCentroid: Float
/// Overall RMS level
var rmsLevel: Float
// MARK: - Initialization
/// Creates an empty AudioAnalysisData with default values
static var empty: AudioAnalysisData {
AudioAnalysisData(
fftMagnitudes: [],
melBands: Array(repeating: 0, count: 64),
subBassEnergy: 0,
subBassHistory: [],
sidechainEnvelope: 0,
sidechainPumpAmount: 0,
isPumping: false,
hnrRatio: 0.5,
isPeak: false,
peakIntensity: 0,
leftChannel: [],
rightChannel: [],
spectralCentroid: 0.5,
rmsLevel: 0
)
}
/// Creates AudioAnalysisData with specified FFT size
static func create(fftSize: Int) -> AudioAnalysisData {
AudioAnalysisData(
fftMagnitudes: Array(repeating: 0, count: fftSize / 2),
melBands: Array(repeating: 0, count: 64),
subBassEnergy: 0,
subBassHistory: Array(repeating: 0, count: 128),
sidechainEnvelope: 0,
sidechainPumpAmount: 0,
isPumping: false,
hnrRatio: 0.5,
isPeak: false,
peakIntensity: 0,
leftChannel: [],
rightChannel: [],
spectralCentroid: 0.5,
rmsLevel: 0
)
}
}
@@ -0,0 +1,109 @@
//
// VisualizationMode.swift
// PsytranceVisualizer
//
// Enumeration of all available visualization modes
//
import Foundation
/// Available visualization modes, accessible via keyboard shortcuts 1-8
enum VisualizationMode: Int, CaseIterable, Codable {
case fftClassic = 1
case melSpectrogram = 2
case subBass = 3
case sidechainPump = 4
case hnr = 5
case mandelbrot = 6
case tunnelWarp = 7
case dmtGeometry = 8
/// Display name for UI
var displayName: String {
switch self {
case .fftClassic:
return "FFT Classic"
case .melSpectrogram:
return "Mel Spektrogramm"
case .subBass:
return "Sub-Bass (<100Hz)"
case .sidechainPump:
return "Sidechain Pump"
case .hnr:
return "Harmonic/Noise"
case .mandelbrot:
return "Mandelbrot"
case .tunnelWarp:
return "Tunnel Warp"
case .dmtGeometry:
return "DMT Geometry"
}
}
/// Keyboard shortcut (1-8)
var shortcut: String {
return "\(self.rawValue)"
}
/// Metal shader function name
var shaderFunctionName: String {
switch self {
case .fftClassic:
return "fftClassicFragment"
case .melSpectrogram:
return "melSpectrogramFragment"
case .subBass:
return "subBassFragment"
case .sidechainPump:
return "sidechainPumpFragment"
case .hnr:
return "hnrFragment"
case .mandelbrot:
return "mandelbrotFragment"
case .tunnelWarp:
return "tunnelWarpFragment"
case .dmtGeometry:
return "dmtGeometryFragment"
}
}
/// Description of the visualization
var description: String {
switch self {
case .fftClassic:
return "Classic frequency spectrum bars with glow effects"
case .melSpectrogram:
return "64-band Mel spectrogram with scrolling waterfall display"
case .subBass:
return "Pulsating rings visualizing sub-bass energy below 100Hz"
case .sidechainPump:
return "Breathing zoom effect synchronized to sidechain pumping"
case .hnr:
return "Harmonic vs noise visualization with geometric shapes"
case .mandelbrot:
return "Audio-reactive Mandelbrot fractal with zoom and color cycling"
case .tunnelWarp:
return "Infinite tunnel effect with warp distortion"
case .dmtGeometry:
return "Sacred geometry patterns: Flower of Life, Metatron's Cube, Sri Yantra"
}
}
/// Creates mode from keyboard key code
static func fromKeyCode(_ keyCode: UInt16) -> VisualizationMode? {
// Key codes for 1-8 on US keyboard
let keyCodes: [UInt16: Int] = [
18: 1, // 1
19: 2, // 2
20: 3, // 3
21: 4, // 4
23: 5, // 5
22: 6, // 6
26: 7, // 7
28: 8 // 8
]
guard let modeNumber = keyCodes[keyCode] else { return nil }
return VisualizationMode(rawValue: modeNumber)
}
}