Skip to content

Commit 4cc59b9

Browse files
renal128cursoragent
andcommitted
fix: align orb reactions with conversation audio
Make mic input drive angular motion and agent output drive shape changes while hardening shader edges and Int16 sampling. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4913cd2 commit 4cc59b9

2 files changed

Lines changed: 23 additions & 18 deletions

File tree

Sources/ElevenLabsWidget/Audio/ConversationAudioLevelMonitor.swift

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ final class ConversationAudioLevelMonitor: ConversationAudioObserver, @unchecked
1212
private static let release: Float = 0.75
1313

1414
private let lock = NSLock()
15+
private let scratchLock = NSLock()
1516
private var storedLevel: Float = 0
16-
/// Only touched from the audio thread, which delivers buffers serially.
17+
/// Reused for Int16 conversion.
1718
private var scratch: [Float] = []
1819

1920
/// The loudest level since the last read, leaving one release step behind.
@@ -43,14 +44,16 @@ final class ConversationAudioLevelMonitor: ConversationAudioObserver, @unchecked
4344
if let channel = buffer.floatChannelData?[0] {
4445
vDSP_rmsqv(channel, stride, &rms, vDSP_Length(frames))
4546
} else if let channel = buffer.int16ChannelData?[0] {
46-
// Grown, never reallocated per buffer: this is the audio thread.
47-
if scratch.count < frames { scratch = [Float](repeating: 0, count: frames) }
48-
scratch.withUnsafeMutableBufferPointer { floats in
49-
guard let samples = floats.baseAddress else { return }
50-
vDSP_vflt16(channel, stride, samples, 1, vDSP_Length(frames))
51-
var scale = 1 / Float(Int16.max)
52-
vDSP_vsmul(samples, 1, &scale, samples, 1, vDSP_Length(frames))
53-
vDSP_rmsqv(samples, 1, &rms, vDSP_Length(frames))
47+
scratchLock.withLock {
48+
// Grown, never reallocated per buffer: this is the audio thread.
49+
if scratch.count < frames { scratch = [Float](repeating: 0, count: frames) }
50+
scratch.withUnsafeMutableBufferPointer { floats in
51+
guard let samples = floats.baseAddress else { return }
52+
vDSP_vflt16(channel, stride, samples, 1, vDSP_Length(frames))
53+
var scale = 1 / Float(Int16.max)
54+
vDSP_vsmul(samples, 1, &scale, samples, 1, vDSP_Length(frames))
55+
vDSP_rmsqv(samples, 1, &rms, vDSP_Length(frames))
56+
}
5457
}
5558
} else {
5659
return nil

Sources/ElevenLabsWidget/Resources/OrbShader.metal

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ bool drawOval(float2 polarUv, float2 polarCenter, float a, float b, bool reverse
5858
float2 p = polarUv - polarCenter;
5959
float oval = (p.x * p.x) / (a * a) + (p.y * p.y) / (b * b);
6060

61-
float edge = smoothstep(1.0, 1.0 - softness, oval);
61+
float edge = 1.0 - smoothstep(1.0 - softness, 1.0, oval);
6262

6363
if (edge > 0.0) {
6464
float gradient = reverseGradient ? (1.0 - (p.x / a + 1.0) / 2.0) : ((p.x / a + 1.0) / 2.0);
@@ -134,7 +134,9 @@ fragment float4 orbFragmentShader(VertexOut in [[stage_in]],
134134
);
135135

136136
float noise = flow(decomposed, radius * 0.03 - uniforms.animation * 0.2) - 0.5;
137-
theta += noise * mix(0.5, 1.0, uniforms.outputVolume);
137+
float inputResponse = sqrt(uniforms.inputVolume);
138+
float angularStrength = mix(0.1, 6.0, inputResponse);
139+
theta += noise * angularStrength;
138140

139141
float4 color = float4(1.0, 1.0, 1.0, 1.0);
140142

@@ -151,7 +153,7 @@ fragment float4 orbFragmentShader(VertexOut in [[stage_in]],
151153
for (int i = 0; i < 7; i++) {
152154
float noise = perlinTexture(float2(fmod(centers[i] + uniforms.time * 0.05, 1.0), 0.5));
153155
a = 0.5 + noise * 0.5; // Increased variance: goes from 0.0 to 1.0
154-
b = noise * mix(4.5, 3.0, uniforms.inputVolume); // Tall semi-minor axis
156+
b = noise * mix(4.5, 3.0, uniforms.outputVolume); // Tall semi-minor axis
155157
bool reverseGradient = (i % 2 == 1); // Reverse gradient for every second oval
156158

157159
// Calculate the distance in polar coordinates
@@ -174,13 +176,13 @@ fragment float4 orbFragmentShader(VertexOut in [[stage_in]],
174176
float ringRadius1 = sharpRing(decomposed, uniforms.time * 0.1);
175177
float ringRadius2 = smoothRing(decomposed, uniforms.time * 0.1);
176178

177-
float inputRadius1 = radius + uniforms.inputVolume * 0.3;
178-
float inputRadius2 = radius + uniforms.inputVolume * 0.2;
179-
float opacity1 = mix(0.3, 0.8, uniforms.inputVolume);
180-
float opacity2 = mix(0.25, 0.6, uniforms.inputVolume);
179+
float outputRadius1 = radius + uniforms.outputVolume * 0.3;
180+
float outputRadius2 = radius + uniforms.outputVolume * 0.2;
181+
float opacity1 = mix(0.3, 0.8, uniforms.outputVolume);
182+
float opacity2 = mix(0.25, 0.6, uniforms.outputVolume);
181183

182-
float ringAlpha1 = (inputRadius2 >= ringRadius1) ? opacity1 : 0.0;
183-
float ringAlpha2 = smoothstep(ringRadius2 - 0.05, ringRadius2 + 0.05, inputRadius1) * opacity2;
184+
float ringAlpha1 = (outputRadius2 >= ringRadius1) ? opacity1 : 0.0;
185+
float ringAlpha2 = smoothstep(ringRadius2 - 0.05, ringRadius2 + 0.05, outputRadius1) * opacity2;
184186

185187
float totalRingAlpha = max(ringAlpha1, ringAlpha2);
186188

0 commit comments

Comments
 (0)