Files
Claude a22c238dc4 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
2025-12-22 21:36:45 +00:00

118 lines
3.5 KiB
Metal

//
// FFTClassicShader.metal
// PsytranceVisualizer
//
// Classic FFT bar visualization with glow effects
//
#include <metal_stdlib>
using namespace metal;
// Include common definitions
#include "Common.metal"
fragment float4 fftClassicFragment(
VertexOut in [[stage_in]],
constant ShaderUniforms& uniforms [[buffer(0)]],
constant float* fftData [[buffer(1)]]
) {
float2 uv = in.uv;
float2 resolution = uniforms.resolution;
float time = uniforms.time;
float reactivity = uniforms.reactivity;
// Number of bars to display
const int numBars = 64;
const float barWidth = 1.0 / float(numBars);
const float barGap = barWidth * 0.2;
const float actualBarWidth = barWidth - barGap;
// Determine which bar this pixel belongs to
int barIndex = int(uv.x * float(numBars));
barIndex = clamp(barIndex, 0, numBars - 1);
// Get FFT magnitude for this bar (with some averaging for smoothness)
float magnitude = fftData[barIndex];
// Apply reactivity scaling
magnitude = magnitude * (0.5 + reactivity * 1.5);
magnitude = clamp(magnitude, 0.0, 1.0);
// Calculate bar position within its cell
float barCellX = fract(uv.x * float(numBars));
float barCenterX = 0.5;
// Distance from bar center (for width calculation)
float distFromCenter = abs(barCellX - barCenterX);
float halfWidth = actualBarWidth * 0.5 / barWidth;
// Check if we're inside the bar horizontally
bool insideBarX = distFromCenter < halfWidth;
// Bar height from bottom
float barHeight = magnitude;
// Add some bounce on peaks
if (uniforms.isPeak > 0.5) {
barHeight += uniforms.peakIntensity * 0.1 * sin(time * 20.0 + float(barIndex) * 0.3);
}
// Check if we're inside the bar vertically (from bottom)
float yFromBottom = 1.0 - uv.y;
bool insideBarY = yFromBottom < barHeight;
// Color based on frequency and magnitude
float colorPhase = float(barIndex) / float(numBars) + time * 0.05;
float3 barColor = psytrancePalette(colorPhase, time);
// Intensity gradient from bottom to top
float intensityGradient = yFromBottom / max(barHeight, 0.01);
intensityGradient = clamp(intensityGradient, 0.0, 1.0);
// Make top of bars brighter
barColor = mix(barColor * 0.6, barColor * 1.5, intensityGradient);
// Calculate glow
float glowRadius = 0.05 * (1.0 + magnitude);
float distToBar = 0.0;
if (!insideBarX) {
distToBar = (distFromCenter - halfWidth) * barWidth;
}
if (!insideBarY && yFromBottom >= barHeight) {
float vertDist = yFromBottom - barHeight;
distToBar = max(distToBar, vertDist);
}
float glow = exp(-distToBar * distToBar / (glowRadius * glowRadius * 2.0));
glow *= magnitude;
// Final color
float3 finalColor = float3(0.0);
if (insideBarX && insideBarY) {
// Inside the bar
finalColor = barColor;
// Add peak cap (bright line at top)
float capThickness = 0.01;
if (abs(yFromBottom - barHeight) < capThickness) {
finalColor = float3(1.0); // White cap
}
} else {
// Add glow outside bars
finalColor = barColor * glow * 0.5;
}
// Add subtle background pulse with sub-bass
float bgPulse = uniforms.subBassEnergy * 0.05;
finalColor += deepPurple * bgPulse;
// Add overall glow at peaks
if (uniforms.isPeak > 0.5) {
finalColor += neonMagenta * uniforms.peakIntensity * 0.1;
}
return float4(finalColor, 1.0);
}