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,279 @@
//
// MetalRenderer.swift
// PsytranceVisualizer
//
// Metal-based renderer for all visualization modes
//
import MetalKit
import simd
/// Uniform data passed to all shaders
struct ShaderUniforms {
var time: Float
var resolution: SIMD2<Float>
var reactivity: Float
// Audio analysis data
var subBassEnergy: Float
var sidechainPump: Float
var sidechainEnvelope: Float
var hnrRatio: Float
var isPeak: Float
var peakIntensity: Float
var spectralCentroid: Float
var rmsLevel: Float
// Visualization mode (1-8)
var mode: Int32
// Padding for Metal alignment
var padding: SIMD2<Float> = .zero
}
/// Metal renderer managing all visualization shaders
final class MetalRenderer: NSObject, ObservableObject {
// MARK: - Properties
private let device: MTLDevice
private let commandQueue: MTLCommandQueue
private var pipelineStates: [VisualizationMode: MTLRenderPipelineState] = [:]
private var currentPipelineState: MTLRenderPipelineState?
@Published private(set) var currentMode: VisualizationMode = .fftClassic
// MARK: - Buffers
private var uniformBuffer: MTLBuffer?
private var fftBuffer: MTLBuffer?
private var melBuffer: MTLBuffer?
private var subBassHistoryBuffer: MTLBuffer?
// MARK: - State
private var startTime: CFAbsoluteTime
private var uniforms = ShaderUniforms(
time: 0,
resolution: SIMD2<Float>(1920, 1080),
reactivity: 0.5,
subBassEnergy: 0,
sidechainPump: 0,
sidechainEnvelope: 0,
hnrRatio: 0.5,
isPeak: 0,
peakIntensity: 0,
spectralCentroid: 0.5,
rmsLevel: 0,
mode: 1
)
private var audioData: AudioAnalysisData = .empty
// MARK: - Constants
private let maxFFTSize = 1024
private let melBandCount = 64
private let historySize = 128
// MARK: - Initialization
init?(device: MTLDevice) {
guard let queue = device.makeCommandQueue() else {
print("[MetalRenderer] Failed to create command queue")
return nil
}
self.device = device
self.commandQueue = queue
self.startTime = CFAbsoluteTimeGetCurrent()
super.init()
createBuffers()
loadShaders()
}
// MARK: - Public Methods
/// Sets the current visualization mode
func setVisualizationMode(_ mode: VisualizationMode) {
currentMode = mode
currentPipelineState = pipelineStates[mode]
uniforms.mode = Int32(mode.rawValue)
print("[MetalRenderer] Mode changed to: \(mode.displayName)")
}
/// Updates audio analysis data
func updateAudioData(_ data: AudioAnalysisData) {
audioData = data
// Update uniforms
uniforms.subBassEnergy = data.subBassEnergy
uniforms.sidechainPump = data.sidechainPumpAmount
uniforms.sidechainEnvelope = data.sidechainEnvelope
uniforms.hnrRatio = data.hnrRatio
uniforms.isPeak = data.isPeak ? 1.0 : 0.0
uniforms.peakIntensity = data.peakIntensity
uniforms.spectralCentroid = data.spectralCentroid
uniforms.rmsLevel = data.rmsLevel
// Update FFT buffer
updateFFTBuffer(data.fftMagnitudes)
// Update Mel buffer
updateMelBuffer(data.melBands)
// Update sub-bass history buffer
updateSubBassHistoryBuffer(data.subBassHistory)
}
/// Sets reactivity value
func setReactivity(_ value: Float) {
uniforms.reactivity = max(0.0, min(1.0, value))
}
// MARK: - Private Methods
private func createBuffers() {
// Uniform buffer
uniformBuffer = device.makeBuffer(
length: MemoryLayout<ShaderUniforms>.stride,
options: .storageModeShared
)
// FFT magnitude buffer
fftBuffer = device.makeBuffer(
length: maxFFTSize * MemoryLayout<Float>.stride,
options: .storageModeShared
)
// Mel bands buffer
melBuffer = device.makeBuffer(
length: melBandCount * MemoryLayout<Float>.stride,
options: .storageModeShared
)
// Sub-bass history buffer
subBassHistoryBuffer = device.makeBuffer(
length: historySize * MemoryLayout<Float>.stride,
options: .storageModeShared
)
}
private func updateFFTBuffer(_ magnitudes: [Float]) {
guard let buffer = fftBuffer else { return }
let count = min(magnitudes.count, maxFFTSize)
memcpy(buffer.contents(), magnitudes, count * MemoryLayout<Float>.stride)
}
private func updateMelBuffer(_ bands: [Float]) {
guard let buffer = melBuffer else { return }
let count = min(bands.count, melBandCount)
memcpy(buffer.contents(), bands, count * MemoryLayout<Float>.stride)
}
private func updateSubBassHistoryBuffer(_ history: [Float]) {
guard let buffer = subBassHistoryBuffer else { return }
let count = min(history.count, historySize)
memcpy(buffer.contents(), history, count * MemoryLayout<Float>.stride)
}
private func loadShaders() {
guard let library = device.makeDefaultLibrary() else {
print("[MetalRenderer] Failed to load shader library")
return
}
// Load vertex shader (shared)
guard let vertexFunction = library.makeFunction(name: "vertexShader") else {
print("[MetalRenderer] Failed to load vertex shader")
return
}
// Load all fragment shaders
for mode in VisualizationMode.allCases {
guard let fragmentFunction = library.makeFunction(name: mode.shaderFunctionName) else {
print("[MetalRenderer] Failed to load shader: \(mode.shaderFunctionName)")
continue
}
let descriptor = MTLRenderPipelineDescriptor()
descriptor.vertexFunction = vertexFunction
descriptor.fragmentFunction = fragmentFunction
descriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
// Enable blending for glow effects
descriptor.colorAttachments[0].isBlendingEnabled = true
descriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
descriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
descriptor.colorAttachments[0].sourceAlphaBlendFactor = .one
descriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
do {
let pipelineState = try device.makeRenderPipelineState(descriptor: descriptor)
pipelineStates[mode] = pipelineState
print("[MetalRenderer] Loaded shader: \(mode.displayName)")
} catch {
print("[MetalRenderer] Failed to create pipeline state for \(mode.displayName): \(error)")
}
}
// Set initial pipeline state
currentPipelineState = pipelineStates[.fftClassic]
}
}
// MARK: - MTKViewDelegate
extension MetalRenderer: MTKViewDelegate {
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
uniforms.resolution = SIMD2<Float>(Float(size.width), Float(size.height))
}
func draw(in view: MTKView) {
guard let pipelineState = currentPipelineState,
let drawable = view.currentDrawable,
let renderPassDescriptor = view.currentRenderPassDescriptor else {
return
}
// Update time
uniforms.time = Float(CFAbsoluteTimeGetCurrent() - startTime)
// Update uniform buffer
if let buffer = uniformBuffer {
memcpy(buffer.contents(), &uniforms, MemoryLayout<ShaderUniforms>.stride)
}
// Create command buffer
guard let commandBuffer = commandQueue.makeCommandBuffer(),
let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
return
}
// Set pipeline state
renderEncoder.setRenderPipelineState(pipelineState)
// Set buffers
if let buffer = uniformBuffer {
renderEncoder.setFragmentBuffer(buffer, offset: 0, index: 0)
}
if let buffer = fftBuffer {
renderEncoder.setFragmentBuffer(buffer, offset: 0, index: 1)
}
if let buffer = melBuffer {
renderEncoder.setFragmentBuffer(buffer, offset: 0, index: 2)
}
if let buffer = subBassHistoryBuffer {
renderEncoder.setFragmentBuffer(buffer, offset: 0, index: 3)
}
// Draw fullscreen quad
renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
renderEncoder.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
}
}
@@ -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;
}
@@ -0,0 +1,290 @@
//
// DMTGeometryShader.metal
// PsytranceVisualizer
//
// Sacred geometry patterns: Flower of Life, Metatron's Cube, Sri Yantra, Hexagonal
//
#include <metal_stdlib>
using namespace metal;
#include "Common.metal"
// === SACRED GEOMETRY PRIMITIVES ===
// Flower of Life - overlapping circles
float flowerOfLife(float2 p, float scale, float time) {
p *= scale;
float result = 0.0;
float circleRadius = 0.5;
// Center circle
result = max(result, 1.0 - smoothstep(circleRadius - 0.02, circleRadius, length(p)));
// 6 circles around center
for (int i = 0; i < 6; i++) {
float angle = float(i) * 3.14159 / 3.0 + time * 0.1;
float2 offset = float2(cos(angle), sin(angle)) * circleRadius;
float d = length(p - offset);
result = max(result, 1.0 - smoothstep(circleRadius - 0.02, circleRadius, d));
}
// Second ring of 12 circles
for (int i = 0; i < 12; i++) {
float angle = float(i) * 3.14159 / 6.0 + time * 0.05;
float2 offset = float2(cos(angle), sin(angle)) * circleRadius * 2.0;
float d = length(p - offset);
result = max(result, 0.5 * (1.0 - smoothstep(circleRadius - 0.02, circleRadius, d)));
}
return result;
}
// Metatron's Cube - 13 circles with connecting lines
float metatronsCube(float2 p, float scale, float time) {
p *= scale;
float result = 0.0;
float nodeRadius = 0.08;
float lineWidth = 0.01;
// Define the 13 points of Metatron's Cube
float2 points[13];
points[0] = float2(0.0, 0.0); // Center
// Inner hexagon
for (int i = 0; i < 6; i++) {
float angle = float(i) * 3.14159 / 3.0 + time * 0.1;
points[i + 1] = float2(cos(angle), sin(angle)) * 0.5;
}
// Outer hexagon (rotated)
for (int i = 0; i < 6; i++) {
float angle = float(i) * 3.14159 / 3.0 + 3.14159 / 6.0 + time * 0.1;
points[i + 7] = float2(cos(angle), sin(angle)) * 0.866;
}
// Draw nodes
for (int i = 0; i < 13; i++) {
float d = length(p - points[i]);
float node = 1.0 - smoothstep(nodeRadius - 0.01, nodeRadius, d);
result = max(result, node);
}
// Draw connecting lines
for (int i = 0; i < 13; i++) {
for (int j = i + 1; j < 13; j++) {
float2 a = points[i];
float2 b = points[j];
float2 pa = p - a;
float2 ba = b - a;
float t = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
float d = length(pa - ba * t);
float line = 1.0 - smoothstep(lineWidth, lineWidth + 0.005, d);
result = max(result, line * 0.5);
}
}
return result;
}
// Sri Yantra - 9 interlocking triangles
float sriYantra(float2 p, float scale, float time) {
p *= scale;
float result = 0.0;
float lineWidth = 0.015;
// Rotating factor
float rot = time * 0.05;
// Draw 4 upward triangles
for (int i = 0; i < 4; i++) {
float size = 0.3 + float(i) * 0.15;
float yOffset = -0.1 + float(i) * 0.05;
float2 tp = p - float2(0.0, yOffset);
tp = rotate(tp, rot);
// Triangle SDF
float2 a = float2(0.0, size);
float2 b = float2(-size * 0.866, -size * 0.5);
float2 c = float2(size * 0.866, -size * 0.5);
float d1 = dot(tp - a, normalize(float2(b.y - a.y, a.x - b.x)));
float d2 = dot(tp - b, normalize(float2(c.y - b.y, b.x - c.x)));
float d3 = dot(tp - c, normalize(float2(a.y - c.y, c.x - a.x)));
float triangleDist = max(max(d1, d2), d3);
float edge = 1.0 - smoothstep(0.0, lineWidth, abs(triangleDist));
result = max(result, edge * (1.0 - float(i) * 0.15));
}
// Draw 5 downward triangles
for (int i = 0; i < 5; i++) {
float size = 0.25 + float(i) * 0.12;
float yOffset = 0.1 - float(i) * 0.04;
float2 tp = p - float2(0.0, yOffset);
tp = rotate(tp, -rot);
float2 a = float2(0.0, -size);
float2 b = float2(-size * 0.866, size * 0.5);
float2 c = float2(size * 0.866, size * 0.5);
float d1 = dot(tp - a, normalize(float2(b.y - a.y, a.x - b.x)));
float d2 = dot(tp - b, normalize(float2(c.y - b.y, b.x - c.x)));
float d3 = dot(tp - c, normalize(float2(a.y - c.y, c.x - a.x)));
float triangleDist = max(max(d1, d2), d3);
float edge = 1.0 - smoothstep(0.0, lineWidth, abs(triangleDist));
result = max(result, edge * (1.0 - float(i) * 0.12));
}
// Central bindu (point)
float bindu = 1.0 - smoothstep(0.03, 0.04, length(p));
result = max(result, bindu);
return result;
}
// Hexagonal grid pattern
float hexagonalPattern(float2 p, float scale, float time) {
p *= scale;
// Hexagonal grid transformation
float2 s = float2(1.0, 1.732);
float2 h = s * 0.5;
float2 a = fmod(p, s) - h;
float2 b = fmod(p + h, s) - h;
float2 gv = dot(a, a) < dot(b, b) ? a : b;
float hexDist = max(abs(gv.x), dot(abs(gv), normalize(float2(1.0, 1.732))));
float edge = 1.0 - smoothstep(0.4, 0.42, hexDist);
float fill = smoothstep(0.38, 0.4, hexDist);
// Animate individual hexagons
float2 cellId = floor(p / s);
float cellPhase = hash(cellId + floor(time * 0.5)) * 2.0 * 3.14159;
float pulse = 0.5 + 0.5 * sin(time * 3.0 + cellPhase);
return edge + fill * pulse * 0.3;
}
// === MAIN FRAGMENT SHADER ===
fragment float4 dmtGeometryFragment(
VertexOut in [[stage_in]],
constant ShaderUniforms& uniforms [[buffer(0)]],
constant float* fftData [[buffer(1)]],
constant float* melData [[buffer(2)]],
constant float* historyData [[buffer(3)]]
) {
float2 uv = in.uv;
float2 resolution = uniforms.resolution;
float time = uniforms.time;
float reactivity = uniforms.reactivity;
float subBass = uniforms.subBassEnergy;
float hnr = uniforms.hnrRatio;
float peak = uniforms.isPeak;
float peakIntensity = uniforms.peakIntensity;
// Aspect ratio correction
float aspectRatio = resolution.x / resolution.y;
float2 p = (uv - 0.5) * 2.0;
p.x *= aspectRatio;
// Scale pulsing with sub-bass
float scale = 2.0 + subBass * 0.5 * (0.5 + reactivity * 0.5);
p *= scale;
// Rotation
float rotation = time * 0.1;
p = rotate(p, rotation);
// Determine which geometry to show
// Changes on peaks or every few seconds
float cycleTime = 8.0; // Seconds per geometry
float cyclePhase = fmod(time, cycleTime * 4.0) / cycleTime;
int geometryIndex = int(cyclePhase);
// Force change on strong peaks
if (peak > 0.5 && peakIntensity > 0.7) {
geometryIndex = int(fmod(float(geometryIndex) + 1.0, 4.0));
}
// Calculate all geometries (for blending)
float flower = flowerOfLife(p, 1.0, time);
float metatron = metatronsCube(p, 1.5, time);
float yantra = sriYantra(p, 1.2, time);
float hexGrid = hexagonalPattern(p, 3.0, time);
// Select primary and secondary for blending
float primary = 0.0;
float secondary = 0.0;
float blendPhase = fract(cyclePhase);
switch (geometryIndex) {
case 0:
primary = flower;
secondary = metatron;
break;
case 1:
primary = metatron;
secondary = yantra;
break;
case 2:
primary = yantra;
secondary = hexGrid;
break;
default:
primary = hexGrid;
secondary = flower;
break;
}
// Smooth transition
float transitionWindow = 0.2; // 20% of cycle for transition
float blend = smoothstep(1.0 - transitionWindow, 1.0, blendPhase);
float geometry = mix(primary, secondary, blend);
// Complexity based on HNR (more harmonic = more detail)
geometry *= 0.7 + hnr * 0.3;
// Color based on geometry and audio
float colorPhase = time * 0.1 + geometry * 0.5;
float3 geometryColor = psytrancePalette(colorPhase, time);
// Glow intensity from peak
float glowIntensity = 0.5 + peakIntensity * 0.5;
float3 glowColor = mix(neonMagenta, neonCyan, 0.5 + 0.5 * sin(time));
// Compose final color
float3 finalColor = geometryColor * geometry;
// Add glow
finalColor = addGlow(finalColor, geometry * glowIntensity, glowColor);
// Background - subtle pulsing gradient
float dist = length(uv - 0.5);
float3 bgColor = mix(deepPurple, uvViolet * 0.3, dist);
bgColor *= 0.8 + 0.2 * subBass;
finalColor = mix(bgColor, finalColor, clamp(geometry * 1.5, 0.0, 1.0));
// Peak flash
if (peak > 0.5) {
finalColor += float3(1.0) * peakIntensity * 0.2;
}
// Outer glow
float outerGlow = exp(-dist * 3.0);
finalColor += neonMagenta * outerGlow * 0.1 * subBass;
return float4(finalColor, 1.0);
}
@@ -0,0 +1,117 @@
//
// 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);
}
@@ -0,0 +1,142 @@
//
// HNRShader.metal
// PsytranceVisualizer
//
// Harmonic-to-Noise ratio visualization with geometric shapes vs chaos
//
#include <metal_stdlib>
using namespace metal;
#include "Common.metal"
fragment float4 hnrFragment(
VertexOut in [[stage_in]],
constant ShaderUniforms& uniforms [[buffer(0)]],
constant float* fftData [[buffer(1)]],
constant float* melData [[buffer(2)]],
constant float* historyData [[buffer(3)]]
) {
float2 uv = in.uv;
float2 resolution = uniforms.resolution;
float time = uniforms.time;
float reactivity = uniforms.reactivity;
float hnr = uniforms.hnrRatio;
float subBass = uniforms.subBassEnergy;
// Center coordinates
float2 center = float2(0.5, 0.5);
float aspectRatio = resolution.x / resolution.y;
float2 p = uv - center;
p.x *= aspectRatio;
float dist = length(p);
float angle = atan2(p.y, p.x);
// === HARMONIC SIDE (High HNR = Clear geometric shapes) ===
// Rotating hexagon
float2 rotP = rotate(p, time * 0.5);
float hexDist = sdHexagon(rotP, 0.2 + subBass * 0.1);
float hexEdge = 1.0 - smoothstep(0.0, 0.02, abs(hexDist));
// Inner rotating triangle (star)
float2 rotP2 = rotate(p, -time * 0.3);
float starDist = sdStar(rotP2, 0.12 + subBass * 0.05, 3, 2.5);
float starEdge = 1.0 - smoothstep(0.0, 0.015, abs(starDist));
// Concentric circles
float circles = 0.0;
for (int i = 0; i < 4; i++) {
float radius = 0.1 + float(i) * 0.08 + sin(time + float(i)) * 0.02;
float circleDist = abs(dist - radius);
float circle = 1.0 - smoothstep(0.0, 0.008, circleDist);
circles += circle;
}
// Combine harmonic shapes
float harmonicShapes = hexEdge + starEdge * 0.8 + circles * 0.5;
harmonicShapes = clamp(harmonicShapes, 0.0, 1.0);
// Harmonic color - clean neon
float3 harmonicColor = mix(neonCyan, neonMagenta, 0.5 + 0.5 * sin(angle * 2.0 + time));
// === NOISE SIDE (Low HNR = Chaotic particles) ===
// Noise-based particles
float noiseField = 0.0;
for (int i = 0; i < 5; i++) {
float2 noiseP = p * (3.0 + float(i) * 2.0);
noiseP += time * float(i + 1) * 0.1;
float n = noise(noiseP);
n = pow(n, 2.0);
noiseField += n * (1.0 / float(i + 1));
}
noiseField = clamp(noiseField, 0.0, 1.0);
// Turbulent swirls
float2 turbP = p * 4.0;
float turbulence = fbm(turbP + time * 0.5, 4);
// Chaotic speckles
float speckles = 0.0;
for (int i = 0; i < 30; i++) {
float2 specklePos = float2(
hash(float2(float(i) * 0.1, time * 0.01)) - 0.5,
hash(float2(float(i) * 0.2, time * 0.01 + 0.5)) - 0.5
);
specklePos *= 0.8;
specklePos.x *= aspectRatio;
float speckleDist = length(p - specklePos);
float speckle = exp(-speckleDist * speckleDist * 500.0);
speckle *= hash(float2(float(i), floor(time * 2.0)));
speckles += speckle;
}
float noiseVisual = noiseField * 0.4 + turbulence * 0.3 + speckles * 0.3;
noiseVisual = clamp(noiseVisual, 0.0, 1.0);
// Noise color - harsh, flickering
float3 noiseColor = mix(hotPink, uvViolet, turbulence);
noiseColor *= 0.8 + 0.2 * sin(time * 20.0 + noise(p * 10.0) * 10.0);
// === BLEND based on HNR ===
// HNR determines the mix: 1.0 = pure harmonic, 0.0 = pure noise
float harmonicAmount = hnr;
float noiseAmount = 1.0 - hnr;
// Apply reactivity to make transition more dramatic
harmonicAmount = pow(harmonicAmount, 1.0 / (1.0 + reactivity));
float3 harmonicContrib = harmonicColor * harmonicShapes * harmonicAmount;
float3 noiseContrib = noiseColor * noiseVisual * noiseAmount;
float3 finalColor = harmonicContrib + noiseContrib;
// Add center indicator showing current HNR
float indicator = smoothstep(0.25, 0.24, dist) - smoothstep(0.24, 0.23, dist);
float indicatorFill = smoothstep(0.23, 0.22, dist);
// Split indicator by HNR
float harmonicSide = step(0.0, p.x);
float noiseSide = 1.0 - harmonicSide;
finalColor += neonCyan * indicator * 0.3;
finalColor += neonCyan * indicatorFill * harmonicSide * hnr * 0.2;
finalColor += hotPink * indicatorFill * noiseSide * (1.0 - hnr) * 0.2;
// Background glow
float bgGlow = exp(-dist * dist * 4.0);
float3 bgColor = mix(deepPurple, uvViolet * 0.3, dist);
finalColor += bgColor * (1.0 - clamp(harmonicShapes + noiseVisual, 0.0, 1.0));
// Peak flash
if (uniforms.isPeak > 0.5) {
finalColor += float3(1.0) * uniforms.peakIntensity * 0.15 * exp(-dist * 3.0);
}
return float4(finalColor, 1.0);
}
@@ -0,0 +1,121 @@
//
// MandelbrotShader.metal
// PsytranceVisualizer
//
// Audio-reactive Mandelbrot fractal with zoom and color cycling
//
#include <metal_stdlib>
using namespace metal;
#include "Common.metal"
fragment float4 mandelbrotFragment(
VertexOut in [[stage_in]],
constant ShaderUniforms& uniforms [[buffer(0)]],
constant float* fftData [[buffer(1)]],
constant float* melData [[buffer(2)]],
constant float* historyData [[buffer(3)]]
) {
float2 uv = in.uv;
float2 resolution = uniforms.resolution;
float time = uniforms.time;
float reactivity = uniforms.reactivity;
float subBass = uniforms.subBassEnergy;
float pump = uniforms.sidechainPump;
float centroid = uniforms.spectralCentroid;
// Aspect ratio correction
float aspectRatio = resolution.x / resolution.y;
// Map UV to complex plane
float2 c = (uv - 0.5) * 2.0;
c.x *= aspectRatio;
// Audio-reactive zoom level
// Base zoom increases over time, modulated by sub-bass
float baseZoom = 1.0 + time * 0.02;
float audioZoom = subBass * 0.5 * (0.5 + reactivity * 0.5);
float zoom = pow(2.0, baseZoom + audioZoom);
// Zoom center - drifts based on sidechain
float2 zoomCenter = float2(-0.7, 0.0);
zoomCenter.x += sin(time * 0.1) * 0.3 + pump * 0.1 * sin(time);
zoomCenter.y += cos(time * 0.13) * 0.2 + pump * 0.1 * cos(time);
// Apply zoom
c = c / zoom + zoomCenter;
// Mandelbrot iteration
float2 z = float2(0.0);
int maxIterations = int(50.0 + reactivity * 100.0);
int iterations = 0;
float smoothIter = 0.0;
for (int i = 0; i < 150; i++) {
if (i >= maxIterations) break;
// z = z^2 + c
float2 zNew = float2(
z.x * z.x - z.y * z.y + c.x,
2.0 * z.x * z.y + c.y
);
z = zNew;
float mag2 = dot(z, z);
if (mag2 > 256.0) {
// Smooth iteration count
smoothIter = float(i) - log2(log2(mag2)) + 4.0;
break;
}
iterations = i;
}
// Normalize iteration count
float normalizedIter = smoothIter / float(maxIterations);
// Color based on iterations
float3 color;
if (iterations >= maxIterations - 1) {
// Inside the set - deep color
color = deepPurple * (0.5 + 0.5 * subBass);
} else {
// Outside - color cycling based on iterations and audio
float colorPhase = normalizedIter + time * 0.1 + centroid;
// Use psytrance palette with color rotation
color = psytrancePalette(colorPhase, time);
// Modulate brightness by iteration depth
float brightness = 0.5 + 0.5 * sin(smoothIter * 0.3);
color *= brightness;
// Add glow at boundary
float edgeFactor = 1.0 - normalizedIter;
edgeFactor = pow(edgeFactor, 3.0);
color = addGlow(color, edgeFactor * 0.5, neonCyan);
}
// Sub-bass pulse effect
color *= 0.8 + 0.2 * subBass;
// Sidechain breathing
float breathe = 1.0 + pump * 0.1;
color *= breathe;
// Peak flash in bright areas
if (uniforms.isPeak > 0.5 && iterations < maxIterations - 1) {
color += neonMagenta * uniforms.peakIntensity * 0.2 * normalizedIter;
}
// Subtle vignette
float2 vignetteuv = uv - 0.5;
float vignette = 1.0 - dot(vignetteuv, vignetteuv) * 0.5;
color *= vignette;
return float4(color, 1.0);
}
@@ -0,0 +1,95 @@
//
// MelSpectrogramShader.metal
// PsytranceVisualizer
//
// Mel spectrogram with scrolling waterfall display
//
#include <metal_stdlib>
using namespace metal;
#include "Common.metal"
fragment float4 melSpectrogramFragment(
VertexOut in [[stage_in]],
constant ShaderUniforms& uniforms [[buffer(0)]],
constant float* fftData [[buffer(1)]],
constant float* melData [[buffer(2)]],
constant float* historyData [[buffer(3)]]
) {
float2 uv = in.uv;
float time = uniforms.time;
float reactivity = uniforms.reactivity;
// Configuration
const int numBands = 64;
const int historyLength = 128;
// Map UV to mel band and history position
int bandIndex = int(uv.x * float(numBands));
bandIndex = clamp(bandIndex, 0, numBands - 1);
// Scrolling effect - newer data at bottom
float scrollOffset = fract(time * 0.5); // Scroll speed
float yPos = fract(uv.y + scrollOffset);
// Get mel magnitude
float magnitude = melData[bandIndex];
magnitude = magnitude * (0.5 + reactivity * 1.5);
magnitude = clamp(magnitude, 0.0, 1.0);
// Create waterfall effect using history
int historyIndex = int(yPos * float(historyLength));
historyIndex = clamp(historyIndex, 0, historyLength - 1);
// Combine current and historical data for waterfall
float historicalValue = historyData[historyIndex];
// Blend between current magnitude and position-based intensity
float intensity = magnitude;
// Add some variance based on band position
float bandPhase = float(bandIndex) / float(numBands);
intensity *= 0.8 + 0.2 * sin(bandPhase * 6.28318 + time);
// Apply fade for older data (top of screen)
float ageFade = 1.0 - uv.y * 0.3;
intensity *= ageFade;
// Generate color using heatmap
float3 color = heatmap(intensity);
// Add frequency-dependent hue shift
float hueShift = bandPhase * 0.3;
color = psytrancePalette(intensity + hueShift, time);
// Modulate by actual intensity
color *= 0.3 + intensity * 0.7;
// Add grid lines for visual reference
float gridX = abs(fract(uv.x * float(numBands)) - 0.5) * 2.0;
float gridY = abs(fract(uv.y * 16.0) - 0.5) * 2.0;
float gridLine = smoothstep(0.95, 1.0, gridX) + smoothstep(0.95, 1.0, gridY);
gridLine *= 0.1;
color += float3(gridLine) * uvViolet;
// Add glow on high energy
if (intensity > 0.7) {
float glow = (intensity - 0.7) / 0.3;
color = addGlow(color, glow * 0.5, neonCyan);
}
// Peak flash
if (uniforms.isPeak > 0.5) {
color += neonMagenta * uniforms.peakIntensity * 0.15;
}
// Sub-bass emphasis on lower bands
if (bandIndex < 8) {
color += uvViolet * uniforms.subBassEnergy * 0.3;
}
return float4(color, 1.0);
}
@@ -0,0 +1,130 @@
//
// SidechainPumpShader.metal
// PsytranceVisualizer
//
// Visualizes sidechain pumping with breathing zoom effect
//
#include <metal_stdlib>
using namespace metal;
#include "Common.metal"
fragment float4 sidechainPumpFragment(
VertexOut in [[stage_in]],
constant ShaderUniforms& uniforms [[buffer(0)]],
constant float* fftData [[buffer(1)]],
constant float* melData [[buffer(2)]],
constant float* historyData [[buffer(3)]]
) {
float2 uv = in.uv;
float2 resolution = uniforms.resolution;
float time = uniforms.time;
float reactivity = uniforms.reactivity;
float pump = uniforms.sidechainPump;
float envelope = uniforms.sidechainEnvelope;
float subBass = uniforms.subBassEnergy;
// Center and aspect ratio correction
float2 center = float2(0.5, 0.5);
float aspectRatio = resolution.x / resolution.y;
float2 p = uv - center;
p.x *= aspectRatio;
// Apply breathing zoom effect
float zoomAmount = 1.0 + pump * 0.3 * (0.5 + reactivity * 0.5);
p /= zoomAmount;
// Radial distortion synchronized with pump
float dist = length(p);
float angle = atan2(p.y, p.x);
// Pump-synced radial waves
float radialWave = sin(dist * 15.0 - time * 3.0 + envelope * 10.0);
radialWave *= pump * 0.3;
// Apply distortion
float2 distortedP = p;
distortedP *= 1.0 + radialWave * 0.1;
// Create concentric pulse rings
float rings = 0.0;
const int numRings = 5;
for (int i = 0; i < numRings; i++) {
float ringPhase = fract(time * 0.5 + float(i) * 0.2 - envelope * 0.5);
float ringRadius = ringPhase * 0.6;
float ringWidth = 0.02 + pump * 0.03;
float ringDist = abs(dist - ringRadius);
float ring = exp(-ringDist * ringDist / (ringWidth * ringWidth));
ring *= 1.0 - ringPhase; // Fade out as it expands
ring *= pump;
rings += ring;
}
// Breathing glow in center
float breathIntensity = 0.5 + 0.5 * sin(time * 4.0 + envelope * 6.28318);
breathIntensity *= pump;
float centerGlow = exp(-dist * dist * 8.0);
centerGlow *= breathIntensity;
// Color based on pump phase
float3 pumpColor = mix(uvViolet, neonMagenta, envelope);
float3 ringColor = mix(neonCyan, hotPink, pump);
// Background pattern - angular sectors that pulse
float sectors = 8.0;
float sectorAngle = fract(angle / (2.0 * 3.14159) * sectors);
float sectorPulse = smoothstep(0.4, 0.5, sectorAngle) - smoothstep(0.5, 0.6, sectorAngle);
sectorPulse *= pump * 0.3;
sectorPulse *= exp(-dist * 3.0);
// Spiral pattern
float spiral = fract(angle / (2.0 * 3.14159) * 3.0 + dist * 5.0 - time * 0.5);
spiral = smoothstep(0.4, 0.5, spiral) - smoothstep(0.5, 0.6, spiral);
spiral *= pump * 0.2;
spiral *= exp(-dist * 2.0);
// Compose final color
float3 finalColor = float3(0.0);
// Base gradient
float3 bgGradient = mix(deepPurple, uvViolet * 0.3, dist);
finalColor += bgGradient;
// Add rings
finalColor += ringColor * rings;
// Add center glow
finalColor += pumpColor * centerGlow;
// Add sector pulse
finalColor += neonGreen * sectorPulse;
// Add spiral
finalColor += electricBlue * spiral;
// Screen flash on strong pump
if (pump > 0.7) {
float flash = (pump - 0.7) / 0.3;
flash *= 0.2;
finalColor += neonMagenta * flash;
}
// Peak highlight
if (uniforms.isPeak > 0.5) {
float peakFlash = uniforms.peakIntensity * 0.2;
finalColor += float3(1.0) * peakFlash * exp(-dist * 5.0);
}
// Vignette
float vignette = 1.0 - smoothstep(0.4, 0.8, dist);
finalColor *= 0.7 + vignette * 0.3;
return float4(finalColor, 1.0);
}
@@ -0,0 +1,116 @@
//
// SubBassShader.metal
// PsytranceVisualizer
//
// Pulsating rings visualizing sub-bass energy below 100Hz
//
#include <metal_stdlib>
using namespace metal;
#include "Common.metal"
fragment float4 subBassFragment(
VertexOut in [[stage_in]],
constant ShaderUniforms& uniforms [[buffer(0)]],
constant float* fftData [[buffer(1)]],
constant float* melData [[buffer(2)]],
constant float* historyData [[buffer(3)]]
) {
float2 uv = in.uv;
float2 resolution = uniforms.resolution;
float time = uniforms.time;
float reactivity = uniforms.reactivity;
float subBass = uniforms.subBassEnergy;
// Center coordinates
float2 center = float2(0.5, 0.5);
float aspectRatio = resolution.x / resolution.y;
// Correct for aspect ratio
float2 p = uv - center;
p.x *= aspectRatio;
float dist = length(p);
float angle = atan2(p.y, p.x);
// Main pulsating circle
float baseRadius = 0.15;
float pulseAmount = subBass * (0.5 + reactivity * 0.5);
float mainRadius = baseRadius + pulseAmount * 0.2;
// Add wobble based on angle
float wobble = sin(angle * 4.0 + time * 2.0) * 0.02 * subBass;
mainRadius += wobble;
// Core circle
float coreDist = abs(dist - mainRadius);
float coreGlow = exp(-coreDist * coreDist * 200.0);
// Inner fill with gradient
float innerFill = smoothstep(mainRadius, mainRadius * 0.3, dist);
innerFill *= 0.5 + 0.5 * subBass;
// Expanding rings
const int numRings = 6;
float ringIntensity = 0.0;
for (int i = 0; i < numRings; i++) {
// Each ring expands outward over time
float ringPhase = fract(time * 0.3 - float(i) * 0.15);
float ringRadius = mainRadius + ringPhase * 0.5;
// Get historical sub-bass value for this ring
int histIndex = clamp(int(ringPhase * 64.0), 0, 63);
float histValue = historyData[histIndex];
// Ring thickness based on historical energy
float thickness = 0.005 + histValue * 0.01;
float ringDist = abs(dist - ringRadius);
// Ring visibility
float ring = exp(-ringDist * ringDist / (thickness * thickness));
ring *= (1.0 - ringPhase); // Fade as it expands
ring *= histValue; // Intensity based on history
ringIntensity += ring;
}
// Color composition
float3 coreColor = mix(uvViolet, neonMagenta, subBass);
float3 ringColor = mix(neonMagenta, hotPink, 0.5 + 0.5 * sin(time));
float3 finalColor = float3(0.0);
// Add core
finalColor += coreColor * (innerFill + coreGlow * 2.0);
// Add rings
finalColor += ringColor * ringIntensity * 0.8;
// Add central glow
float centerGlow = exp(-dist * dist * 10.0) * subBass;
finalColor += uvViolet * centerGlow * 0.5;
// Add angular rays on peaks
if (uniforms.isPeak > 0.5) {
float rays = abs(sin(angle * 8.0 + time * 5.0));
rays = pow(rays, 4.0) * exp(-dist * 2.0);
rays *= uniforms.peakIntensity;
finalColor += neonCyan * rays * 0.5;
}
// Outer vignette
float vignette = 1.0 - smoothstep(0.3, 0.8, dist);
finalColor *= vignette;
// Background pulse
float bgPulse = subBass * 0.1;
finalColor += deepPurple * bgPulse;
// Add noise texture for organic feel
float noiseVal = noise(p * 20.0 + time);
finalColor += uvViolet * noiseVal * 0.02 * subBass;
return float4(finalColor, 1.0);
}
@@ -0,0 +1,136 @@
//
// TunnelWarpShader.metal
// PsytranceVisualizer
//
// Infinite tunnel effect with warp distortion
//
#include <metal_stdlib>
using namespace metal;
#include "Common.metal"
fragment float4 tunnelWarpFragment(
VertexOut in [[stage_in]],
constant ShaderUniforms& uniforms [[buffer(0)]],
constant float* fftData [[buffer(1)]],
constant float* melData [[buffer(2)]],
constant float* historyData [[buffer(3)]]
) {
float2 uv = in.uv;
float2 resolution = uniforms.resolution;
float time = uniforms.time;
float reactivity = uniforms.reactivity;
float subBass = uniforms.subBassEnergy;
float pump = uniforms.sidechainPump;
float hnr = uniforms.hnrRatio;
// Center and aspect correction
float aspectRatio = resolution.x / resolution.y;
float2 p = (uv - 0.5) * 2.0;
p.x *= aspectRatio;
// Convert to polar coordinates for tunnel
float dist = length(p);
float angle = atan2(p.y, p.x);
// Avoid division by zero at center
dist = max(dist, 0.001);
// Tunnel depth (inverse of distance)
float depth = 1.0 / dist;
// Speed controlled by sub-bass
float baseSpeed = 2.0;
float audioSpeed = subBass * 3.0 * (0.5 + reactivity * 0.5);
float speed = baseSpeed + audioSpeed;
// Warp distortion from sidechain pump
float warpAmount = pump * 0.5;
depth += sin(angle * 4.0 + time * 2.0) * warpAmount * 0.5;
angle += sin(depth * 2.0 + time) * warpAmount * 0.3;
// Create tunnel coordinates
float2 tunnelUV = float2(
angle / (2.0 * 3.14159) + 0.5, // Angular coordinate [0, 1]
depth + time * speed // Depth with movement
);
// === TUNNEL WALL PATTERNS ===
// Hexagonal grid pattern
float2 hexUV = tunnelUV * float2(8.0, 2.0);
float2 hexCell = floor(hexUV);
float2 hexFrac = fract(hexUV);
// Offset every other row
if (fmod(hexCell.y, 2.0) > 0.5) {
hexFrac.x = fract(hexFrac.x + 0.5);
}
float hexDist = length(hexFrac - 0.5);
float hexPattern = smoothstep(0.4, 0.35, hexDist);
// Add concentric rings
float rings = sin(tunnelUV.y * 20.0) * 0.5 + 0.5;
rings = smoothstep(0.3, 0.7, rings);
// Angular segments
float segments = 8.0;
float angularLines = abs(sin(angle * segments));
angularLines = smoothstep(0.95, 1.0, angularLines);
// Combine patterns
float pattern = hexPattern * 0.5 + rings * 0.3 + angularLines * 0.2;
// === COLORING ===
// Base color cycles with depth and time
float colorPhase = tunnelUV.y * 0.1 + time * 0.2;
float3 tunnelColor = psytrancePalette(colorPhase, time);
// Depth fog (darker towards center/infinity)
float fog = exp(-dist * 2.0);
tunnelColor *= fog;
// Pattern overlay
float3 patternColor = mix(uvViolet, neonCyan, rings);
tunnelColor = mix(tunnelColor, patternColor, pattern * 0.5);
// Edge glow (bright at tunnel edges)
float edgeGlow = exp(-dist * 5.0);
tunnelColor = addGlow(tunnelColor, (1.0 - edgeGlow) * 0.3, neonMagenta);
// Center light (looking into the tunnel)
float centerLight = exp(-dist * dist * 50.0);
tunnelColor += float3(1.0) * centerLight * 0.5;
// HNR affects pattern complexity
float patternIntensity = hnr;
tunnelColor *= 0.7 + patternIntensity * 0.3;
// Add noise for texture
float noiseVal = noise(tunnelUV * 10.0 + time);
tunnelColor += uvViolet * noiseVal * 0.1;
// Pump flash
if (pump > 0.5) {
float pumpFlash = (pump - 0.5) * 2.0;
tunnelColor += neonMagenta * pumpFlash * 0.2;
}
// Peak flash
if (uniforms.isPeak > 0.5) {
float peakFlash = uniforms.peakIntensity;
tunnelColor += float3(1.0) * peakFlash * 0.15 * (1.0 - edgeGlow);
}
// Speed lines effect
float speedLines = fract(tunnelUV.y * 50.0 - time * speed * 2.0);
speedLines = smoothstep(0.95, 1.0, speedLines);
speedLines *= subBass * 0.5;
tunnelColor += neonCyan * speedLines;
return float4(tunnelColor, 1.0);
}