2026-06-19 02:10:25 +02:00
|
|
|
import Foundation
|
|
|
|
|
import AVFoundation
|
|
|
|
|
import VoiceCatCore
|
|
|
|
|
|
|
|
|
|
// MARK: - Helper types
|
|
|
|
|
|
|
|
|
|
struct ChatMessage: Identifiable {
|
|
|
|
|
let id = UUID()
|
|
|
|
|
let timestamp: Date
|
|
|
|
|
let senderName: String
|
|
|
|
|
let text: String
|
|
|
|
|
let scope: VoiceCatTextScope
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct ActivityEntry: Identifiable {
|
|
|
|
|
let id = UUID()
|
|
|
|
|
let timestamp: Date
|
|
|
|
|
let text: String
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct VoiceState {
|
|
|
|
|
var micActive = false
|
|
|
|
|
var selfMuted = false
|
|
|
|
|
var selfDeafened = false
|
feat(ios): audio overhaul, Join/Leave Voice, channel-id sync fix, stereo mic capture
Three iOS client problems fixed plus a new core stereo-mic capture ABI:
1. Channel-id sync bug (mic button permanently dimmed): SessionState never
synced currentChannelId from the self user's channelId on connect, so the
mic button (gated on currentChannelId == 0) stayed dimmed. Added
syncSelfChannel() (mirrors macOS MainWindowController.swift:461,491,522);
called from init/.channelList/.userJoined/.userLeft/.userUpdated/.joinResult.
Added applyServerMuteState() + serverMuted/serverDeafened to VoiceState.
2. Join/Leave Voice button: replaced icon-only mic toggle with explicit
text button (parity with macOS). Mute/deafen disable when not in voice.
3. IOSAudioRouter.swift (new): full AVAudioSession routing layer — input
port selection, built-in mic orientation/polar patterns, Bluetooth
HFP/A2DP/Off modes, Standard/Raw mic processing, stereo capture, AirPlay,
UserDefaults persistence. AudioSessionManager delegates to it.
4. Core stereo-mic capture (append-only ABI): vc_set_capture_channels()
lets the core open the mic device in stereo (2-ch interleaved). LocalStream
gains capture_channels; ensure_audio_running reads it; audio_engine.cpp
capture_accum_ + on_capture updated to channel-aware accumulation. Test
test_stereo_mic_capture (headless, L!=R stereo round-trip). Swift wrapper
VoiceCatClient.setCaptureChannels.
5. Settings UI rework: AVAudioSession-derived input/output tree replaces
miniaudio device picker.
6. iOS deployment target raised to 18.0 (Package.swift + project.pbxproj).
swift-tools-version 6.0 with swiftLanguageModes .v5.
Docs: tech-stack.md, architecture.md, voice.md, roadmap.md, building.md
updated; stale 'vc_audio_suspend/resume deferred' claims corrected.
Verified: ctest --preset dev 21/21 green; swift test 6/6 green;
xcodebuild -target VoiceCatiOS -sdk iphonesimulator BUILD SUCCEEDED.
2026-06-19 13:17:52 +02:00
|
|
|
var serverMuted = false
|
|
|
|
|
var serverDeafened = false
|
2026-06-19 02:10:25 +02:00
|
|
|
var inputMode: VoiceCatInputMode = .voiceActivation
|
|
|
|
|
var vadThreshold: Float = 0.025
|
|
|
|
|
var level: Float = 0.0
|
|
|
|
|
var currentDeviceId: String?
|
|
|
|
|
var localStreamId: UInt32 = 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - SessionState
|
|
|
|
|
|
|
|
|
|
@Observable
|
|
|
|
|
@MainActor
|
|
|
|
|
final class SessionState {
|
|
|
|
|
let client: VoiceCatClient
|
|
|
|
|
let selfUserId: UInt32
|
|
|
|
|
|
|
|
|
|
var channels: [Channel] = []
|
|
|
|
|
var users: [User] = []
|
|
|
|
|
var currentChannelId: UInt32 = 0
|
|
|
|
|
var messages: [ChatMessage] = []
|
|
|
|
|
var activityLog: [ActivityEntry] = []
|
|
|
|
|
var voiceState = VoiceState()
|
|
|
|
|
var permissions: Permissions
|
|
|
|
|
var accounts: [Account] = []
|
|
|
|
|
var devices: [Device] = []
|
|
|
|
|
|
|
|
|
|
init(client: VoiceCatClient, selfUserId: UInt32, permissions: Permissions) {
|
|
|
|
|
self.client = client
|
|
|
|
|
self.selfUserId = selfUserId
|
|
|
|
|
self.permissions = permissions
|
|
|
|
|
AudioSessionManager.shared.client = client
|
|
|
|
|
refreshChannels()
|
|
|
|
|
refreshUsers()
|
feat(ios): audio overhaul, Join/Leave Voice, channel-id sync fix, stereo mic capture
Three iOS client problems fixed plus a new core stereo-mic capture ABI:
1. Channel-id sync bug (mic button permanently dimmed): SessionState never
synced currentChannelId from the self user's channelId on connect, so the
mic button (gated on currentChannelId == 0) stayed dimmed. Added
syncSelfChannel() (mirrors macOS MainWindowController.swift:461,491,522);
called from init/.channelList/.userJoined/.userLeft/.userUpdated/.joinResult.
Added applyServerMuteState() + serverMuted/serverDeafened to VoiceState.
2. Join/Leave Voice button: replaced icon-only mic toggle with explicit
text button (parity with macOS). Mute/deafen disable when not in voice.
3. IOSAudioRouter.swift (new): full AVAudioSession routing layer — input
port selection, built-in mic orientation/polar patterns, Bluetooth
HFP/A2DP/Off modes, Standard/Raw mic processing, stereo capture, AirPlay,
UserDefaults persistence. AudioSessionManager delegates to it.
4. Core stereo-mic capture (append-only ABI): vc_set_capture_channels()
lets the core open the mic device in stereo (2-ch interleaved). LocalStream
gains capture_channels; ensure_audio_running reads it; audio_engine.cpp
capture_accum_ + on_capture updated to channel-aware accumulation. Test
test_stereo_mic_capture (headless, L!=R stereo round-trip). Swift wrapper
VoiceCatClient.setCaptureChannels.
5. Settings UI rework: AVAudioSession-derived input/output tree replaces
miniaudio device picker.
6. iOS deployment target raised to 18.0 (Package.swift + project.pbxproj).
swift-tools-version 6.0 with swiftLanguageModes .v5.
Docs: tech-stack.md, architecture.md, voice.md, roadmap.md, building.md
updated; stale 'vc_audio_suspend/resume deferred' claims corrected.
Verified: ctest --preset dev 21/21 green; swift test 6/6 green;
xcodebuild -target VoiceCatiOS -sdk iphonesimulator BUILD SUCCEEDED.
2026-06-19 13:17:52 +02:00
|
|
|
syncSelfChannel()
|
2026-06-19 02:10:25 +02:00
|
|
|
refreshDevices()
|
|
|
|
|
client.onEvent = { [weak self] ev in
|
|
|
|
|
Task { @MainActor [weak self] in self?.handleEvent(ev) }
|
|
|
|
|
}
|
|
|
|
|
client.onLevel = { [weak self] _, rms in
|
|
|
|
|
Task { @MainActor [weak self] in self?.voiceState.level = rms }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deinit {
|
|
|
|
|
MainActor.assumeIsolated {
|
|
|
|
|
AudioSessionManager.shared.client = nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Event dispatch
|
|
|
|
|
|
|
|
|
|
func handleEvent(_ ev: VoiceCatEvent) {
|
|
|
|
|
switch ev.type {
|
|
|
|
|
case .channelList:
|
|
|
|
|
refreshChannels()
|
feat(ios): audio overhaul, Join/Leave Voice, channel-id sync fix, stereo mic capture
Three iOS client problems fixed plus a new core stereo-mic capture ABI:
1. Channel-id sync bug (mic button permanently dimmed): SessionState never
synced currentChannelId from the self user's channelId on connect, so the
mic button (gated on currentChannelId == 0) stayed dimmed. Added
syncSelfChannel() (mirrors macOS MainWindowController.swift:461,491,522);
called from init/.channelList/.userJoined/.userLeft/.userUpdated/.joinResult.
Added applyServerMuteState() + serverMuted/serverDeafened to VoiceState.
2. Join/Leave Voice button: replaced icon-only mic toggle with explicit
text button (parity with macOS). Mute/deafen disable when not in voice.
3. IOSAudioRouter.swift (new): full AVAudioSession routing layer — input
port selection, built-in mic orientation/polar patterns, Bluetooth
HFP/A2DP/Off modes, Standard/Raw mic processing, stereo capture, AirPlay,
UserDefaults persistence. AudioSessionManager delegates to it.
4. Core stereo-mic capture (append-only ABI): vc_set_capture_channels()
lets the core open the mic device in stereo (2-ch interleaved). LocalStream
gains capture_channels; ensure_audio_running reads it; audio_engine.cpp
capture_accum_ + on_capture updated to channel-aware accumulation. Test
test_stereo_mic_capture (headless, L!=R stereo round-trip). Swift wrapper
VoiceCatClient.setCaptureChannels.
5. Settings UI rework: AVAudioSession-derived input/output tree replaces
miniaudio device picker.
6. iOS deployment target raised to 18.0 (Package.swift + project.pbxproj).
swift-tools-version 6.0 with swiftLanguageModes .v5.
Docs: tech-stack.md, architecture.md, voice.md, roadmap.md, building.md
updated; stale 'vc_audio_suspend/resume deferred' claims corrected.
Verified: ctest --preset dev 21/21 green; swift test 6/6 green;
xcodebuild -target VoiceCatiOS -sdk iphonesimulator BUILD SUCCEEDED.
2026-06-19 13:17:52 +02:00
|
|
|
syncSelfChannel()
|
|
|
|
|
case .userJoined, .userLeft:
|
2026-06-19 02:10:25 +02:00
|
|
|
refreshUsers()
|
feat(ios): audio overhaul, Join/Leave Voice, channel-id sync fix, stereo mic capture
Three iOS client problems fixed plus a new core stereo-mic capture ABI:
1. Channel-id sync bug (mic button permanently dimmed): SessionState never
synced currentChannelId from the self user's channelId on connect, so the
mic button (gated on currentChannelId == 0) stayed dimmed. Added
syncSelfChannel() (mirrors macOS MainWindowController.swift:461,491,522);
called from init/.channelList/.userJoined/.userLeft/.userUpdated/.joinResult.
Added applyServerMuteState() + serverMuted/serverDeafened to VoiceState.
2. Join/Leave Voice button: replaced icon-only mic toggle with explicit
text button (parity with macOS). Mute/deafen disable when not in voice.
3. IOSAudioRouter.swift (new): full AVAudioSession routing layer — input
port selection, built-in mic orientation/polar patterns, Bluetooth
HFP/A2DP/Off modes, Standard/Raw mic processing, stereo capture, AirPlay,
UserDefaults persistence. AudioSessionManager delegates to it.
4. Core stereo-mic capture (append-only ABI): vc_set_capture_channels()
lets the core open the mic device in stereo (2-ch interleaved). LocalStream
gains capture_channels; ensure_audio_running reads it; audio_engine.cpp
capture_accum_ + on_capture updated to channel-aware accumulation. Test
test_stereo_mic_capture (headless, L!=R stereo round-trip). Swift wrapper
VoiceCatClient.setCaptureChannels.
5. Settings UI rework: AVAudioSession-derived input/output tree replaces
miniaudio device picker.
6. iOS deployment target raised to 18.0 (Package.swift + project.pbxproj).
swift-tools-version 6.0 with swiftLanguageModes .v5.
Docs: tech-stack.md, architecture.md, voice.md, roadmap.md, building.md
updated; stale 'vc_audio_suspend/resume deferred' claims corrected.
Verified: ctest --preset dev 21/21 green; swift test 6/6 green;
xcodebuild -target VoiceCatiOS -sdk iphonesimulator BUILD SUCCEEDED.
2026-06-19 13:17:52 +02:00
|
|
|
syncSelfChannel()
|
|
|
|
|
case .userUpdated:
|
|
|
|
|
refreshUsers()
|
|
|
|
|
syncSelfChannel()
|
|
|
|
|
if let me = users.first(where: { $0.id == selfUserId }) {
|
|
|
|
|
applyServerMuteState(muted: me.serverMuted, deafened: me.serverDeafened)
|
|
|
|
|
}
|
2026-06-19 02:10:25 +02:00
|
|
|
case .textMessage:
|
|
|
|
|
let sender = users.first(where: { $0.id == ev.userId })?.nickname ?? "Unknown"
|
|
|
|
|
messages.append(ChatMessage(
|
|
|
|
|
timestamp: Date(timeIntervalSince1970: Double(ev.timestampUnixMs) / 1000),
|
|
|
|
|
senderName: sender,
|
|
|
|
|
text: ev.text ?? "",
|
|
|
|
|
scope: ev.textScope))
|
|
|
|
|
case .talkState:
|
|
|
|
|
let talking = ev.u32a != 0
|
|
|
|
|
let who = users.first(where: { $0.id == ev.userId })?.nickname ?? "user \(ev.userId)"
|
|
|
|
|
addActivity(talking ? "\(who) started talking" : "\(who) stopped talking")
|
|
|
|
|
case .streamStarted:
|
2026-06-19 13:46:20 +02:00
|
|
|
// A remote user started a stream — ensure the audio session is active so we can
|
|
|
|
|
// hear them even if we haven't joined voice ourselves. Previously the session was
|
|
|
|
|
// only activated when the user pressed Join Voice, so remote audio was silent.
|
|
|
|
|
if ev.userId != selfUserId {
|
|
|
|
|
do {
|
|
|
|
|
try AudioSessionManager.shared.ensureSessionActive()
|
|
|
|
|
} catch {
|
|
|
|
|
addActivity("Audio session activate failed: \(error)")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-19 02:10:25 +02:00
|
|
|
addActivity("Stream started (user \(ev.userId))")
|
|
|
|
|
case .streamStopped:
|
|
|
|
|
addActivity("Stream stopped (user \(ev.userId))")
|
|
|
|
|
case .joinResult:
|
|
|
|
|
if ev.result == .ok {
|
|
|
|
|
currentChannelId = ev.channelId
|
|
|
|
|
addActivity("Joined channel")
|
|
|
|
|
refreshUsers()
|
|
|
|
|
} else {
|
|
|
|
|
addActivity("Join failed: \(ev.result.description)")
|
|
|
|
|
}
|
|
|
|
|
case .error:
|
|
|
|
|
addActivity("Error: \(ev.text ?? ev.result.description)")
|
|
|
|
|
case .genericResult:
|
|
|
|
|
if ev.result != .ok {
|
|
|
|
|
addActivity("Operation failed: \(ev.result.description)")
|
|
|
|
|
}
|
|
|
|
|
case .accountList:
|
|
|
|
|
accounts = client.listAccounts()
|
|
|
|
|
default:
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func addActivity(_ text: String) {
|
|
|
|
|
activityLog.append(ActivityEntry(timestamp: Date(), text: text))
|
|
|
|
|
if activityLog.count > 500 { activityLog.removeFirst() }
|
|
|
|
|
}
|
|
|
|
|
|
feat(ios): audio overhaul, Join/Leave Voice, channel-id sync fix, stereo mic capture
Three iOS client problems fixed plus a new core stereo-mic capture ABI:
1. Channel-id sync bug (mic button permanently dimmed): SessionState never
synced currentChannelId from the self user's channelId on connect, so the
mic button (gated on currentChannelId == 0) stayed dimmed. Added
syncSelfChannel() (mirrors macOS MainWindowController.swift:461,491,522);
called from init/.channelList/.userJoined/.userLeft/.userUpdated/.joinResult.
Added applyServerMuteState() + serverMuted/serverDeafened to VoiceState.
2. Join/Leave Voice button: replaced icon-only mic toggle with explicit
text button (parity with macOS). Mute/deafen disable when not in voice.
3. IOSAudioRouter.swift (new): full AVAudioSession routing layer — input
port selection, built-in mic orientation/polar patterns, Bluetooth
HFP/A2DP/Off modes, Standard/Raw mic processing, stereo capture, AirPlay,
UserDefaults persistence. AudioSessionManager delegates to it.
4. Core stereo-mic capture (append-only ABI): vc_set_capture_channels()
lets the core open the mic device in stereo (2-ch interleaved). LocalStream
gains capture_channels; ensure_audio_running reads it; audio_engine.cpp
capture_accum_ + on_capture updated to channel-aware accumulation. Test
test_stereo_mic_capture (headless, L!=R stereo round-trip). Swift wrapper
VoiceCatClient.setCaptureChannels.
5. Settings UI rework: AVAudioSession-derived input/output tree replaces
miniaudio device picker.
6. iOS deployment target raised to 18.0 (Package.swift + project.pbxproj).
swift-tools-version 6.0 with swiftLanguageModes .v5.
Docs: tech-stack.md, architecture.md, voice.md, roadmap.md, building.md
updated; stale 'vc_audio_suspend/resume deferred' claims corrected.
Verified: ctest --preset dev 21/21 green; swift test 6/6 green;
xcodebuild -target VoiceCatiOS -sdk iphonesimulator BUILD SUCCEEDED.
2026-06-19 13:17:52 +02:00
|
|
|
// MARK: - Self-channel / server-mute sync
|
|
|
|
|
|
|
|
|
|
/// Sync currentChannelId from the self user's channelId in the user list. Mirrors macOS
|
|
|
|
|
/// MainWindowController.swift:461,491,522. The server auto-places every authed user into
|
|
|
|
|
/// the Lobby (channel 1) on connect, but without this sync currentChannelId stays 0 and
|
|
|
|
|
/// the mic button (gated on currentChannelId == 0) stays permanently dimmed.
|
|
|
|
|
private func syncSelfChannel() {
|
|
|
|
|
if let me = users.first(where: { $0.id == selfUserId }) {
|
|
|
|
|
currentChannelId = me.channelId
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Apply server-side mute/deafen state — mirrors macOS MainWindowController.swift:693-700.
|
|
|
|
|
/// iOS was previously ignoring server mute/deafen entirely.
|
|
|
|
|
private func applyServerMuteState(muted: Bool, deafened: Bool) {
|
|
|
|
|
if muted && !voiceState.serverMuted { addActivity("You have been server-muted") }
|
|
|
|
|
if deafened && !voiceState.serverDeafened { addActivity("You have been server-deafened") }
|
|
|
|
|
if !muted && voiceState.serverMuted { addActivity("Server mute cleared") }
|
|
|
|
|
if !deafened && voiceState.serverDeafened { addActivity("Server deafen cleared") }
|
|
|
|
|
voiceState.serverMuted = muted
|
|
|
|
|
voiceState.serverDeafened = deafened
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-19 02:10:25 +02:00
|
|
|
// MARK: - Data refresh
|
|
|
|
|
|
|
|
|
|
func refreshChannels() { channels = client.listChannels() }
|
|
|
|
|
func refreshUsers() { users = client.listUsers() }
|
|
|
|
|
func refreshDevices() { devices = client.listDevices(.input) }
|
|
|
|
|
|
|
|
|
|
// MARK: - Voice controls
|
|
|
|
|
|
|
|
|
|
func joinChannel(_ channelId: UInt32, password: String = "") {
|
|
|
|
|
client.joinChannel(channelId, password: password.isEmpty ? nil : password)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func leaveChannel() {
|
|
|
|
|
client.leaveChannel()
|
|
|
|
|
currentChannelId = 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func startMicStream() {
|
|
|
|
|
AVAudioApplication.requestRecordPermission { [weak self] granted in
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
guard let self else { return }
|
|
|
|
|
if granted {
|
|
|
|
|
self.doStartMicStream()
|
|
|
|
|
} else {
|
|
|
|
|
self.addActivity("Microphone permission denied — grant in Settings > Privacy > Microphone")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func doStartMicStream() {
|
|
|
|
|
do {
|
2026-06-19 13:46:20 +02:00
|
|
|
try AudioSessionManager.shared.ensureSessionActive()
|
2026-06-19 02:10:25 +02:00
|
|
|
} catch {
|
|
|
|
|
addActivity("AVAudioSession activate failed: \(error)")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
let desc = StreamDescriptor(kind: .mic, deviceId: voiceState.currentDeviceId, label: "Mic")
|
|
|
|
|
let (result, streamId) = client.startStream(desc)
|
|
|
|
|
if result == .ok {
|
|
|
|
|
voiceState.micActive = true
|
|
|
|
|
voiceState.localStreamId = streamId
|
feat(ios): audio overhaul, Join/Leave Voice, channel-id sync fix, stereo mic capture
Three iOS client problems fixed plus a new core stereo-mic capture ABI:
1. Channel-id sync bug (mic button permanently dimmed): SessionState never
synced currentChannelId from the self user's channelId on connect, so the
mic button (gated on currentChannelId == 0) stayed dimmed. Added
syncSelfChannel() (mirrors macOS MainWindowController.swift:461,491,522);
called from init/.channelList/.userJoined/.userLeft/.userUpdated/.joinResult.
Added applyServerMuteState() + serverMuted/serverDeafened to VoiceState.
2. Join/Leave Voice button: replaced icon-only mic toggle with explicit
text button (parity with macOS). Mute/deafen disable when not in voice.
3. IOSAudioRouter.swift (new): full AVAudioSession routing layer — input
port selection, built-in mic orientation/polar patterns, Bluetooth
HFP/A2DP/Off modes, Standard/Raw mic processing, stereo capture, AirPlay,
UserDefaults persistence. AudioSessionManager delegates to it.
4. Core stereo-mic capture (append-only ABI): vc_set_capture_channels()
lets the core open the mic device in stereo (2-ch interleaved). LocalStream
gains capture_channels; ensure_audio_running reads it; audio_engine.cpp
capture_accum_ + on_capture updated to channel-aware accumulation. Test
test_stereo_mic_capture (headless, L!=R stereo round-trip). Swift wrapper
VoiceCatClient.setCaptureChannels.
5. Settings UI rework: AVAudioSession-derived input/output tree replaces
miniaudio device picker.
6. iOS deployment target raised to 18.0 (Package.swift + project.pbxproj).
swift-tools-version 6.0 with swiftLanguageModes .v5.
Docs: tech-stack.md, architecture.md, voice.md, roadmap.md, building.md
updated; stale 'vc_audio_suspend/resume deferred' claims corrected.
Verified: ctest --preset dev 21/21 green; swift test 6/6 green;
xcodebuild -target VoiceCatiOS -sdk iphonesimulator BUILD SUCCEEDED.
2026-06-19 13:17:52 +02:00
|
|
|
// Apply the user's capture channel selection (mono/stereo) from IOSAudioRouter.
|
|
|
|
|
// The core opens the capture device via miniaudio on the next engine start;
|
|
|
|
|
// vc_set_capture_channels tells it to open in stereo (2) or mono (1).
|
|
|
|
|
let channels = IOSAudioRouter.shared.captureChannels.channelCount
|
|
|
|
|
if channels != 1 {
|
|
|
|
|
client.setCaptureChannels(streamId: streamId, channels: channels)
|
|
|
|
|
}
|
2026-06-19 02:10:25 +02:00
|
|
|
} else {
|
|
|
|
|
addActivity("Failed to start mic: \(result.description)")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func stopMicStream() {
|
|
|
|
|
if voiceState.localStreamId != 0 {
|
|
|
|
|
client.stopStream(voiceState.localStreamId)
|
|
|
|
|
voiceState.localStreamId = 0
|
|
|
|
|
}
|
|
|
|
|
voiceState.micActive = false
|
|
|
|
|
voiceState.level = 0
|
2026-06-19 13:46:20 +02:00
|
|
|
// Do NOT deactivate the AVAudioSession here — the user may still want to hear
|
|
|
|
|
// remote audio (other people talking). The session is deactivated only when
|
|
|
|
|
// disconnecting from the server (see AppState.disconnect / .disconnected event).
|
2026-06-19 02:10:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setMute(_ muted: Bool, deafened: Bool) {
|
|
|
|
|
client.setSelfMute(micMuted: muted, deafened: deafened)
|
|
|
|
|
voiceState.selfMuted = muted
|
|
|
|
|
voiceState.selfDeafened = deafened
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setInputMode(_ mode: VoiceCatInputMode) {
|
|
|
|
|
client.setInputMode(mode)
|
|
|
|
|
voiceState.inputMode = mode
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setVadThreshold(_ threshold: Float) {
|
|
|
|
|
client.setVadThreshold(threshold)
|
|
|
|
|
voiceState.vadThreshold = threshold
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setPushToTalk(_ active: Bool) {
|
|
|
|
|
client.setPushToTalk(active)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Text
|
|
|
|
|
|
|
|
|
|
func sendText(_ text: String, scope: VoiceCatTextScope, targetId: UInt32 = 0) {
|
|
|
|
|
client.sendText(scope: scope, targetId: targetId, text: text)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Admin
|
|
|
|
|
|
|
|
|
|
func kickUser(_ userId: UInt32, reason: String) {
|
|
|
|
|
client.kickUser(userId, reason: reason.isEmpty ? nil : reason)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func banUser(_ userId: UInt32, reason: String, expiresUnixMs: UInt64) {
|
|
|
|
|
client.banUser(userId, reason: reason.isEmpty ? nil : reason, expiresUnixMs: expiresUnixMs)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func moveUser(_ userId: UInt32, toChannel channelId: UInt32) {
|
|
|
|
|
client.moveUser(userId, toChannel: channelId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setPermissions(_ userId: UInt32, perms: Permissions) {
|
|
|
|
|
client.setPermission(userId, perms: perms)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setServerMute(_ userId: UInt32, muted: Bool, deafened: Bool) {
|
|
|
|
|
client.setServerMute(userId, muted: muted, deafened: deafened)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func createChannel(name: String, topic: String) {
|
|
|
|
|
let info = ChannelEdit(id: 0, parentId: 0, name: name, topic: topic,
|
|
|
|
|
passwordProtected: false, password: nil,
|
|
|
|
|
maxUsers: 0, sortOrder: 0, audio: AudioConfig())
|
|
|
|
|
client.createChannel(info)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func deleteChannel(_ channelId: UInt32) {
|
|
|
|
|
client.deleteChannel(channelId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fetchAccountList() {
|
|
|
|
|
client.requestAccountList()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func createAccount(username: String, password: String) {
|
|
|
|
|
client.createAccount(username, password: password)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func deleteAccount(username: String) {
|
|
|
|
|
client.deleteAccount(username)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func resetPassword(username: String, newPassword: String) {
|
|
|
|
|
client.resetPassword(username, newPassword: newPassword)
|
|
|
|
|
}
|
|
|
|
|
}
|