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,241 @@
|
||||
//
|
||||
// Common.metal
|
||||
// PsytranceVisualizer
|
||||
//
|
||||
// Shared shader functions, types, and psytrance color palette
|
||||
//
|
||||
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
// MARK: - Uniforms Structure
|
||||
|
||||
struct ShaderUniforms {
|
||||
float time;
|
||||
float2 resolution;
|
||||
float reactivity;
|
||||
|
||||
float subBassEnergy;
|
||||
float sidechainPump;
|
||||
float sidechainEnvelope;
|
||||
float hnrRatio;
|
||||
float isPeak;
|
||||
float peakIntensity;
|
||||
float spectralCentroid;
|
||||
float rmsLevel;
|
||||
|
||||
int mode;
|
||||
float2 padding;
|
||||
};
|
||||
|
||||
// MARK: - Vertex Data
|
||||
|
||||
struct VertexOut {
|
||||
float4 position [[position]];
|
||||
float2 uv;
|
||||
};
|
||||
|
||||
// MARK: - Psytrance Color Palette
|
||||
|
||||
constant float3 neonMagenta = float3(1.0, 0.0, 1.0);
|
||||
constant float3 neonCyan = float3(0.0, 1.0, 1.0);
|
||||
constant float3 neonGreen = float3(0.224, 1.0, 0.078);
|
||||
constant float3 uvViolet = float3(0.482, 0.0, 1.0);
|
||||
constant float3 hotPink = float3(1.0, 0.2, 0.6);
|
||||
constant float3 electricBlue = float3(0.0, 0.5, 1.0);
|
||||
constant float3 deepPurple = float3(0.1, 0.0, 0.15);
|
||||
|
||||
// MARK: - Palette Functions
|
||||
|
||||
inline float3 getPaletteColor(int index) {
|
||||
switch (index % 6) {
|
||||
case 0: return neonMagenta;
|
||||
case 1: return neonCyan;
|
||||
case 2: return neonGreen;
|
||||
case 3: return uvViolet;
|
||||
case 4: return hotPink;
|
||||
default: return electricBlue;
|
||||
}
|
||||
}
|
||||
|
||||
inline float3 rainbowPalette(float t) {
|
||||
float3 a = float3(0.5, 0.5, 0.5);
|
||||
float3 b = float3(0.5, 0.5, 0.5);
|
||||
float3 c = float3(1.0, 1.0, 1.0);
|
||||
float3 d = float3(0.0, 0.33, 0.67);
|
||||
return a + b * cos(6.28318 * (c * t + d));
|
||||
}
|
||||
|
||||
inline float3 psytrancePalette(float t, float time) {
|
||||
// Cycle through psytrance colors
|
||||
float phase = fract(t + time * 0.1);
|
||||
|
||||
if (phase < 0.2) {
|
||||
return mix(uvViolet, neonMagenta, phase * 5.0);
|
||||
} else if (phase < 0.4) {
|
||||
return mix(neonMagenta, hotPink, (phase - 0.2) * 5.0);
|
||||
} else if (phase < 0.6) {
|
||||
return mix(hotPink, neonCyan, (phase - 0.4) * 5.0);
|
||||
} else if (phase < 0.8) {
|
||||
return mix(neonCyan, neonGreen, (phase - 0.6) * 5.0);
|
||||
} else {
|
||||
return mix(neonGreen, uvViolet, (phase - 0.8) * 5.0);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Heatmap for Spectrogram
|
||||
|
||||
inline float3 heatmap(float t) {
|
||||
// Low energy: dark purple
|
||||
// High energy: white through neon colors
|
||||
if (t < 0.2) {
|
||||
return mix(float3(0.05, 0.0, 0.1), uvViolet, t * 5.0);
|
||||
} else if (t < 0.4) {
|
||||
return mix(uvViolet, neonMagenta, (t - 0.2) * 5.0);
|
||||
} else if (t < 0.6) {
|
||||
return mix(neonMagenta, hotPink, (t - 0.4) * 5.0);
|
||||
} else if (t < 0.8) {
|
||||
return mix(hotPink, neonCyan, (t - 0.6) * 5.0);
|
||||
} else {
|
||||
return mix(neonCyan, float3(1.0), (t - 0.8) * 5.0);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Noise Functions
|
||||
|
||||
// Simplex-like noise
|
||||
inline float hash(float2 p) {
|
||||
float3 p3 = fract(float3(p.xyx) * 0.1031);
|
||||
p3 += dot(p3, p3.yzx + 33.33);
|
||||
return fract((p3.x + p3.y) * p3.z);
|
||||
}
|
||||
|
||||
inline float noise(float2 p) {
|
||||
float2 i = floor(p);
|
||||
float2 f = fract(p);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
|
||||
float a = hash(i);
|
||||
float b = hash(i + float2(1.0, 0.0));
|
||||
float c = hash(i + float2(0.0, 1.0));
|
||||
float d = hash(i + float2(1.0, 1.0));
|
||||
|
||||
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
||||
}
|
||||
|
||||
inline float fbm(float2 p, int octaves) {
|
||||
float value = 0.0;
|
||||
float amplitude = 0.5;
|
||||
float frequency = 1.0;
|
||||
|
||||
for (int i = 0; i < octaves; i++) {
|
||||
value += amplitude * noise(p * frequency);
|
||||
frequency *= 2.0;
|
||||
amplitude *= 0.5;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// 3D noise for volumetric effects
|
||||
inline float noise3D(float3 p) {
|
||||
float3 i = floor(p);
|
||||
float3 f = fract(p);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
|
||||
float2 uv = i.xy + float2(37.0, 17.0) * i.z;
|
||||
float a = hash(uv);
|
||||
float b = hash(uv + float2(1.0, 0.0));
|
||||
float c = hash(uv + float2(0.0, 1.0));
|
||||
float d = hash(uv + float2(1.0, 1.0));
|
||||
|
||||
float2 uv2 = uv + float2(37.0, 17.0);
|
||||
float e = hash(uv2);
|
||||
float ff = hash(uv2 + float2(1.0, 0.0));
|
||||
float g = hash(uv2 + float2(0.0, 1.0));
|
||||
float h = hash(uv2 + float2(1.0, 1.0));
|
||||
|
||||
float x1 = mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
||||
float x2 = mix(mix(e, ff, f.x), mix(g, h, f.x), f.y);
|
||||
|
||||
return mix(x1, x2, f.z);
|
||||
}
|
||||
|
||||
// MARK: - Utility Functions
|
||||
|
||||
inline float2 rotate(float2 p, float angle) {
|
||||
float c = cos(angle);
|
||||
float s = sin(angle);
|
||||
return float2(p.x * c - p.y * s, p.x * s + p.y * c);
|
||||
}
|
||||
|
||||
inline float map(float value, float inMin, float inMax, float outMin, float outMax) {
|
||||
return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin);
|
||||
}
|
||||
|
||||
inline float smoothstepEdge(float edge0, float edge1, float x) {
|
||||
float t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
|
||||
return t * t * (3.0 - 2.0 * t);
|
||||
}
|
||||
|
||||
// MARK: - Glow Effect
|
||||
|
||||
inline float3 addGlow(float3 color, float intensity, float3 glowColor) {
|
||||
return color + glowColor * intensity * intensity;
|
||||
}
|
||||
|
||||
// MARK: - SDF Functions for Geometry
|
||||
|
||||
inline float sdCircle(float2 p, float r) {
|
||||
return length(p) - r;
|
||||
}
|
||||
|
||||
inline float sdBox(float2 p, float2 b) {
|
||||
float2 d = abs(p) - b;
|
||||
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
|
||||
}
|
||||
|
||||
inline float sdHexagon(float2 p, float r) {
|
||||
const float3 k = float3(-0.866025404, 0.5, 0.577350269);
|
||||
p = abs(p);
|
||||
p -= 2.0 * min(dot(k.xy, p), 0.0) * k.xy;
|
||||
p -= float2(clamp(p.x, -k.z * r, k.z * r), r);
|
||||
return length(p) * sign(p.y);
|
||||
}
|
||||
|
||||
inline float sdStar(float2 p, float r, int n, float m) {
|
||||
float an = 3.141593 / float(n);
|
||||
float en = 3.141593 / m;
|
||||
float2 acs = float2(cos(an), sin(an));
|
||||
float2 ecs = float2(cos(en), sin(en));
|
||||
|
||||
float bn = fmod(atan2(p.x, p.y), 2.0 * an) - an;
|
||||
p = length(p) * float2(cos(bn), abs(sin(bn)));
|
||||
p -= r * acs;
|
||||
p += ecs * clamp(-dot(p, ecs), 0.0, r * acs.y / ecs.y);
|
||||
return length(p) * sign(p.x);
|
||||
}
|
||||
|
||||
// MARK: - Vertex Shader (Fullscreen Quad)
|
||||
|
||||
vertex VertexOut vertexShader(uint vertexID [[vertex_id]]) {
|
||||
// Generate fullscreen quad
|
||||
float2 positions[4] = {
|
||||
float2(-1.0, -1.0),
|
||||
float2( 1.0, -1.0),
|
||||
float2(-1.0, 1.0),
|
||||
float2( 1.0, 1.0)
|
||||
};
|
||||
|
||||
float2 uvs[4] = {
|
||||
float2(0.0, 1.0),
|
||||
float2(1.0, 1.0),
|
||||
float2(0.0, 0.0),
|
||||
float2(1.0, 0.0)
|
||||
};
|
||||
|
||||
VertexOut out;
|
||||
out.position = float4(positions[vertexID], 0.0, 1.0);
|
||||
out.uv = uvs[vertexID];
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user