136 lines
4.0 KiB
Swift
136 lines
4.0 KiB
Swift
import SwiftUI
|
|
|
|
struct ContentView: View {
|
|
@StateObject private var streamManager: AudioStreamManager
|
|
@StateObject private var captureManager = AudioCaptureManager()
|
|
|
|
@State private var isPressing = false
|
|
|
|
init() {
|
|
let manager = AudioStreamManager(endpoint: URL(string: "ws://192.168.68.126:8642/stream")!)
|
|
_streamManager = StateObject(wrappedValue: manager)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 14) {
|
|
Text("Hermes")
|
|
.font(.headline)
|
|
|
|
Text(statusText)
|
|
.font(.caption2)
|
|
.foregroundStyle(statusColor)
|
|
.lineLimit(2)
|
|
.multilineTextAlignment(.center)
|
|
.frame(minHeight: 28)
|
|
|
|
Circle()
|
|
.fill(buttonFill)
|
|
.overlay {
|
|
Image(systemName: isPressing ? "waveform" : "mic.fill")
|
|
.font(.system(size: 36, weight: .semibold))
|
|
.foregroundStyle(.white)
|
|
}
|
|
.overlay {
|
|
Circle()
|
|
.stroke(.white.opacity(isPressing ? 0.9 : 0.25), lineWidth: 3)
|
|
}
|
|
.frame(width: 112, height: 112)
|
|
.scaleEffect(isPressing ? 0.94 : 1.0)
|
|
.animation(.spring(response: 0.2, dampingFraction: 0.75), value: isPressing)
|
|
.gesture(
|
|
DragGesture(minimumDistance: 0)
|
|
.onChanged { _ in
|
|
guard !isPressing else { return }
|
|
isPressing = true
|
|
startPTT()
|
|
}
|
|
.onEnded { _ in
|
|
isPressing = false
|
|
stopPTT()
|
|
}
|
|
)
|
|
|
|
Text(isPressing ? "Release to send" : "Hold to talk")
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding()
|
|
.onAppear {
|
|
streamManager.connect()
|
|
captureManager.onPCMChunk = { [streamManager] chunk in
|
|
streamManager.sendPCMChunk(chunk)
|
|
}
|
|
}
|
|
.onDisappear {
|
|
captureManager.stopRecording()
|
|
streamManager.disconnect()
|
|
}
|
|
}
|
|
|
|
private var statusText: String {
|
|
switch captureManager.state {
|
|
case .recording:
|
|
return "Listening"
|
|
case .starting:
|
|
return "Starting mic"
|
|
case .failed(let message):
|
|
return message
|
|
case .idle:
|
|
break
|
|
}
|
|
|
|
switch streamManager.playbackState {
|
|
case .playing:
|
|
return "Playing response"
|
|
case .failed(let message):
|
|
return message
|
|
case .idle:
|
|
break
|
|
}
|
|
|
|
switch streamManager.connectionState {
|
|
case .connected:
|
|
return "Ready"
|
|
case .connecting:
|
|
return "Connecting"
|
|
case .disconnected:
|
|
return "Disconnected"
|
|
case .failed(let message):
|
|
return message
|
|
}
|
|
}
|
|
|
|
private var statusColor: Color {
|
|
if case .failed = captureManager.state { return .red }
|
|
if case .failed = streamManager.connectionState { return .red }
|
|
if case .failed = streamManager.playbackState { return .red }
|
|
if captureManager.state == .recording { return .green }
|
|
if streamManager.playbackState == .playing { return .blue }
|
|
return .secondary
|
|
}
|
|
|
|
private var buttonFill: Color {
|
|
if captureManager.state == .recording { return .red }
|
|
if streamManager.playbackState == .playing { return .blue }
|
|
return .accentColor
|
|
}
|
|
|
|
private func startPTT() {
|
|
streamManager.connect()
|
|
streamManager.sendControlEvent("start")
|
|
|
|
Task {
|
|
await captureManager.startRecording()
|
|
}
|
|
}
|
|
|
|
private func stopPTT() {
|
|
captureManager.stopRecording()
|
|
streamManager.sendControlEvent("stop")
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
ContentView()
|
|
}
|