Voice ring
A ring of light for voice interfaces. A glassy spectrum that bends with the voice, takes the colour of how it feels and leans toward whoever is speaking, read by the Voice direction model. SwiftUI, on device.
Installation
iOS 17+ / macOS 14+. Build through Xcode: the edge lens is a Metal shader compiled by Xcode's build system, not by SwiftPM alone. Add NSMicrophoneUsageDescription to your Info.plist.
// Package.swift, or in Xcode: File › Add Package Dependencies… › paste the URL (Branch: main) .package(url: "https://github.com/Jakubantalik/Libraries.dev", branch: "main") // …and in your target's dependencies: .product(name: "VoiceRingKit", package: "Libraries.dev")
Usage
import VoiceRingKit
struct VoiceScreen: View {
@State private var meter = VoiceMeter()
@State private var state: VoiceState = .listening
var body: some View {
VoiceRing(state: state, source: .meter(meter), size: 240)
.task { try? await meter.start() } // asks for the microphone
}
}
// States: .idle, .listening, .speaking (the assistant's playback), .processing
// Drive it yourself instead of the microphone:
VoiceRing(state: .speaking, source: .level { player.level })
Connect a model: Emotion
Pro, as on Voice: the ring takes the colour of how the voice feels, green for positive, teal for calm, red for negative. emotion2vec+ reads the tone of the last two seconds on device; GoEmotions reads the live transcript. Add
import VoiceRingEmotion // .product(name: "VoiceRingEmotion", package: "Libraries.dev")
@State private var emotion = EmotionListener()
VoiceRing(state: .listening, source: .meter(meter), mood: emotion.mood)
.task {
try? await meter.start()
await emotion.start(meter: meter) // tone (emotion2vec+) and words (GoEmotions)
}
// Or a fixed mood: mood: .happy / .calm / .angry / .sad, or emotion: .positive
Emotion2Vec.mlpackage (and, for the words, TextEmotion.mlpackage with the TextEmotionTokenizer folder and NSSpeechRecognitionUsageDescription) to your app target; the converted models come with Pro. Any model that is missing is skipped.
Connect a model: Voice direction
Our own on-device model reads which side of the iPhone a voice comes from, from its stereo microphones: Voice direction (beta, trained and tested on iPhone 15 Pro so far). VoiceRingKit carries no direction code: without VoiceDirectionKit the ring still listens and leans only when you pass
// 1. Download VoiceDirection.mfxl (free account) and add it to your app target.
// 2. Add VoiceDirectionKit, read the meter the ring listens to, pass the side in:
// no calibration (beta: tested on iPhone 15 Pro; on other iPhones, calibrate).
import VoiceRingKit
import VoiceDirectionKit
@State private var meter = VoiceMeter()
@State private var voice = VoiceDirection()
VoiceRing(state: .listening, source: .meter(meter), direction: voice.pan)
.task { try? await voice.start(meter) } // opens the meter in stereo
// voice.pan: −1 left … 1 right · voice.side: .left / .ahead / .right · voice.probabilities
direction. Without the model file, voice.calibrate(.left), .ahead and .right calibrate the current pose instead. Physical iPhone only; in the Simulator, pass direction yourself.
Options
state.idlewaits with a slow drift,.listeningand.speakingfollow the source,.processingspins the spectrum with its highlight running round like a loader. Changes ease, never cut.source.meter(meter)(the microphone, stereo on iPhone),.level { 0…1 }sampled every frame, or.silence.mood,emotionThe colour the spectrum shifts toward; nil stays the plain spectrum.direction−1 left … 1 right; nil reads the meter's own estimate. While a voice is heard the ring drifts that way and meets the screen's edge through a glass lens.styleAVoiceStyle:speed,sensitivity,threshold,release,idle,theme(.auto/.dark/.light) and the ring's own knobs inopts:rings(6),lean(0.91),ringGap(0.02),deform(1.45),halo(0.5),radius(0.56). The Studio writes these for you.
License
VoiceRingKit, VoiceRingEmotion and VoiceDirectionKit are MIT. The models are licensed apart from the code: emotion2vec+ under the FunASR Model Open Source License (keep its credit in your app's acknowledgements), roberta-base-go_emotions under MIT, and the Voice direction model under the Libraries.dev Model License: free for development and non-commercial apps, a commercial release needs Pro or Business.
Add Voice ring from Libraries.dev to my SwiftUI app: a voice-reactive ring of
light for a voice mode (the assistant's orb).
Install (Swift Package Manager, iOS 17+ / macOS 14+, build through Xcode):
.package(url: "https://github.com/Jakubantalik/Libraries.dev", branch: "main")
.product(name: "VoiceRingKit", package: "Libraries.dev")
Add NSMicrophoneUsageDescription to Info.plist.
Usage:
import VoiceRingKit
struct VoiceScreen: View {
@State private var meter = VoiceMeter()
var body: some View {
VoiceRing(state: .listening, source: .meter(meter), size: 240)
.task { try? await meter.start() } // asks for the microphone
}
}
- state: .idle | .listening | .speaking | .processing (changes ease)
- source: .meter(meter) (microphone, stereo on iPhone) | .level { 0...1 } | .silence
- mood: VoiceMood? (valence -1...1, arousal 0...1; .happy .calm .angry .sad)
or emotion: VoiceOrbEmotion? (.positive .negative .calm): the spectrum's colour
- direction: Double? (-1 left ... 1 right; nil = the meter's own estimate)
- style: VoiceStyle (speed, sensitivity, threshold, release, idle, theme,
opts: rings 6, lean 0.91, ringGap 0.02, deform 1.45, halo 0.5, radius 0.56)
Emotion (Pro): .product(name: "VoiceRingEmotion", package: "Libraries.dev");
@State var emotion = EmotionListener(); await emotion.start(meter: meter);
pass mood: emotion.mood. Needs Emotion2Vec.mlpackage (emotion2vec+, FunASR
Model License) and optionally TextEmotion.mlpackage + TextEmotionTokenizer in
the app target; ask the user for them (they come with Libraries.dev Pro).
Direction (Voice direction model): ask the user to download
VoiceDirection.mfxl from https://libraries.dev/voice-direction.html (free
account) and add it to the app target, plus
.product(name: "VoiceDirectionKit", package: "Libraries.dev"):
@State var meter = VoiceMeter(); @State var voice = VoiceDirection();
.task { try? await voice.start(meter) }; pass source: .meter(meter),
direction: voice.pan. No calibration (beta: tested on iPhone 15 Pro; on other
iPhones use voice.calibrate(.left / .ahead / .right)). VoiceRingKit alone has
no direction code; direction: is nil unless you pass it. Never commit the model to a
public repo. Model License: free for development and non-commercial apps; a
commercial release needs Libraries.dev Pro or Business.
The kit is MIT. Docs: https://libraries.dev/voice-ring.html