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:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user