Use when you have a written implementation plan to execute in a separate session with review checkpoints
npx skills add 4Players/odin-agent-skills --skill "odin-voice-swift"
Install specific skill from multi-skill repository
# Description
|
# SKILL.md
name: odin-voice-swift
description: |
ODIN Voice Swift SDK (OdinKit) for real-time voice chat in iOS and macOS apps.
Use when: implementing voice chat in Swift/SwiftUI projects, working with OdinRoom/OdinPeer classes,
handling delegates or Combine publishers, configuring audio processing.
Requires iOS 9+/macOS 10.15+, Swift 5.0+. For ODIN concepts, see odin-fundamentals skill.
license: MIT
ODIN Voice Swift SDK (OdinKit)
Swift wrapper for real-time VoIP chat on iOS and macOS.
Requirements
- iOS 9.0+ / macOS 10.15+
- Xcode 10.2+
- Swift 5.0+
Quick Start
// 1. Create access key (do this on backend in production)
let accessKey = try OdinAccessKey("YOUR_ACCESS_KEY")
// 2. Generate token
let token = try accessKey.generateToken(roomId: "MyRoom", userId: "user123")
// 3. Create room and set delegate
let room = OdinRoom()
room.delegate = self
// 4. Join room
try room.join(token: token)
// 5. Add microphone
let mediaId = try room.addMedia(type: .Audio)
Key Classes
OdinAccessKey
Manages authentication credentials.
// Create from string
let accessKey = try OdinAccessKey("YOUR_44_CHAR_ACCESS_KEY")
// Properties
accessKey.rawValue // Original string
accessKey.id // Key identifier
accessKey.publicKey // Public key bytes
accessKey.secretKey // Secret key bytes
// Generate token
let token = try accessKey.generateToken(roomId: "RoomName", userId: "UserId")
OdinRoom
Virtual communication space.
// Properties
room.id // Room identifier
room.customer // Customer identifier
room.userData // Room user data
room.connectionStatus // Current status
room.peers // Connected peers
room.medias // Audio streams
room.ownPeer // Self reference
// Observable (Combine)
room.$connectionStatus // Published status
room.$peers // Published peers
room.$medias // Published medias
// Methods
try room.join(token: token)
room.leave()
let mediaId = try room.addMedia(type: .Audio)
try room.removeMedia(streamHandle: mediaId)
try room.updateUserData(userData: bytes, target: .Peer)
try room.updatePosition(x: 100, y: 50)
room.setPositionScale(scale: 1.0)
try room.sendMessage(data: bytes, targetIds: [])
room.setAudioAutopilotMode(.Room)
OdinPeer
Represents a connected participant.
// Properties
peer.id // Unique peer ID
peer.userId // User identifier
peer.userData // Peer metadata
peer.medias // Associated media streams
peer.activeMedias // Currently active streams
// Self reference
let myPeer = room.ownPeer
OdinMedia
Audio stream (local or remote).
// Properties
media.streamHandle // Handle for operations
media.id // Media identifier
media.peerId // Owner peer ID
media.remote // Is from remote peer
media.type // Media type (.Audio)
media.activityStatus // Currently active/speaking
media.audioNode // For audio mixing
OdinManager
Manages multiple rooms.
// Singleton
let manager = OdinManager.sharedInstance()
// Properties
manager.rooms // [String: OdinRoom] dictionary
Event Handling
Option A: Delegate Pattern
Implement OdinRoomDelegate:
class VoiceManager: OdinRoomDelegate {
func onRoomConnectionStateChanged(room: OdinRoom, oldState: OdinRoomConnectionState,
newState: OdinRoomConnectionState, reason: OdinRoomConnectionStateChangeReason) {
print("Connection: \(oldState) → \(newState)")
}
func onRoomJoined(room: OdinRoom, ownPeer: OdinPeer) {
print("Joined as peer \(ownPeer.id)")
}
func onPeerJoined(room: OdinRoom, peer: OdinPeer) {
print("Peer joined: \(peer.userId)")
}
func onPeerLeft(room: OdinRoom, peer: OdinPeer) {
print("Peer left: \(peer.userId)")
}
func onMediaAdded(room: OdinRoom, peer: OdinPeer, media: OdinMedia) {
print("Media added from \(peer.userId)")
}
func onMediaRemoved(room: OdinRoom, peer: OdinPeer, media: OdinMedia) {
print("Media removed from \(peer.userId)")
}
func onMediaActiveStateChanged(room: OdinRoom, peer: OdinPeer, media: OdinMedia) {
print("\(peer.userId) is \(media.activityStatus ? "speaking" : "silent")")
}
func onMessageReceived(room: OdinRoom, senderId: UInt64, data: [UInt8]) {
let text = String(bytes: data, encoding: .utf8) ?? ""
print("Message: \(text)")
}
func onRoomUserDataChanged(room: OdinRoom) {
print("Room data updated")
}
func onPeerUserDataChanged(room: OdinRoom, peer: OdinPeer) {
print("Peer \(peer.userId) data updated")
}
}
Option B: Combine Publishers
import Combine
var cancellables = Set<AnyCancellable>()
room.$connectionStatus
.sink { status in
print("Status: \(status)")
}
.store(in: &cancellables)
room.$peers
.sink { peers in
print("Peers: \(peers.count)")
}
.store(in: &cancellables)
Audio Processing (APM)
var apmConfig = OdinApmConfig()
// Voice Activity Detection
apmConfig.voiceActivityDetection = true
apmConfig.vadAttackProbability = 0.9
apmConfig.vadReleaseProbability = 0.8
// Volume Gate
apmConfig.volumeGate = true
apmConfig.volumeGateAttackThreshold = -30 // dBFS
apmConfig.volumeGateReleaseThreshold = -40
// Audio Processing
apmConfig.echoCancellation = true
apmConfig.noiseSuppression = .Moderate // or .Aggressive, .VeryAggressive
apmConfig.highPassFilter = true
apmConfig.preamplifier = false
apmConfig.transientSuppression = true
apmConfig.gainController = .V2
// Apply config
try room.updateAudioConfig(apmConfig)
User Data & Messaging
Setting User Data
// Using OdinCustomData helper
let userData = try OdinCustomData.encode(["name": "Alice", "level": 10])
try room.updateUserData(userData: userData, target: .Peer)
// Reading user data
if let data = peer.userData {
let decoded = try OdinCustomData.decode(data) as [String: Any]
let name = decoded["name"] as? String
}
Sending Messages
// Broadcast to all
let message = Array("Hello everyone!".utf8)
try room.sendMessage(data: message, targetIds: [])
// Send to specific peers
try room.sendMessage(data: message, targetIds: [peer1.id, peer2.id])
Proximity Audio
For large multiplayer spaces with local voice clustering:
// Update position (2D coordinates)
try room.updatePosition(x: 100.0, y: 50.0)
// Set scale (affects culling radius)
room.setPositionScale(scale: 1.0)
// Culling: Peers within unit circle (1.0) can hear each other
Audio Integration
OdinKit integrates with AVAudioEngine:
// Access audio nodes for mixing
let roomAudioNode = room.audioNode
let mediaAudioNode = media.audioNode
// Custom sample rate
let config = OdinAudioStreamConfig(sampleRate: 48000)
let mediaId = try room.addMedia(audioConfig: config)
SwiftUI Example
struct VoiceView: View {
@StateObject var voiceManager = VoiceManager()
var body: some View {
VStack {
ForEach(voiceManager.peers, id: \.id) { peer in
HStack {
Text(peer.userId)
if peer.activeMedias.count > 0 {
Image(systemName: "mic.fill")
}
}
}
Button("Join Room") {
voiceManager.joinRoom()
}
}
}
}
Documentation
API reference: https://docs.4players.io/voice/swift/api/
SwiftUI sample: https://docs.4players.io/voice/swift/samples/swiftui-sample
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.