Files
Ai/FT991A-Remote/FT991A-Remote/Views/Panels/AudioPanel.swift
T
Claude 1e153f2f85 Add FT-991A Remote Control App for macOS
Complete Phase 1 implementation of the Yaesu FT-991A remote control
application with CAT protocol support over USB serial (CP210x).

Features implemented:
- SerialPortManager with auto-detection of CP210x ports
- Full CAT protocol parser and command builder
- RadioState model with all transceiver parameters
- Modern SwiftUI interface with frequency/mode/level controls
- Skeuomorphic front panel view (switchable)
- Debug panel with CAT command console
- QSO log panel with CSV export/import
- Audio routing panel with BlackHole integration
- Settings with connection, UI, keyboard configuration
- Menu bar extra for background operation
- German/English localization
- Logging system for debugging

Supports: Frequency control, VFO A/B, all modes (LSB/USB/CW/FM/AM/
DATA/RTTY/C4FM), level controls, NB/NR/DNF/ATU/Split functions,
S-meter/Power/SWR metering, PTT control via Shift key.

Target: macOS 15.0+ (Sequoia/Tahoe)
2025-12-18 10:59:15 +00:00

149 lines
5.5 KiB
Swift

//
// AudioPanel.swift
// FT991A-Remote
//
// BlackHole audio routing panel
//
import SwiftUI
// MARK: - Audio Panel
struct AudioPanel: View {
@StateObject private var audioRouter = AudioRouter()
@EnvironmentObject var settingsController: SettingsController
var body: some View {
VStack(spacing: 0) {
// Header
HStack {
Text("Audio Routing")
.font(.headline)
Spacer()
Button {
audioRouter.refreshDevices()
} label: {
Image(systemName: "arrow.clockwise")
}
.help("Geräte aktualisieren")
}
.padding(.horizontal)
.padding(.vertical, 8)
.background(Color.secondary.opacity(0.1))
Divider()
ScrollView {
VStack(alignment: .leading, spacing: 16) {
// BlackHole Status
GroupBox("BlackHole Status") {
HStack {
Circle()
.fill(audioRouter.isBlackHoleInstalled ? Color.green : Color.red)
.frame(width: 12, height: 12)
Text(audioRouter.isBlackHoleInstalled ? "Installiert" : "Nicht gefunden")
Spacer()
if !audioRouter.isBlackHoleInstalled {
Link("Installieren", destination: URL(string: "https://existential.audio/blackhole/")!)
.font(.caption)
}
}
.padding(.vertical, 4)
if let device = audioRouter.blackHoleDevice {
Text("Gerät: \(device.name)")
.font(.caption)
.foregroundColor(.secondary)
}
}
// Input Device
GroupBox("Eingang (RX Audio)") {
Picker("Eingabegerät", selection: $audioRouter.selectedInputDevice) {
Text("Keines").tag(nil as AudioDeviceID?)
ForEach(audioRouter.inputDevices) { device in
Text(device.displayName).tag(device.id as AudioDeviceID?)
}
}
.pickerStyle(.menu)
if let ft991a = audioRouter.ft991aDevice {
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text("FT-991A erkannt: \(ft991a.name)")
.font(.caption)
}
}
}
// Output Device
GroupBox("Ausgang (TX Audio)") {
Picker("Ausgabegerät", selection: $audioRouter.selectedOutputDevice) {
Text("Keines").tag(nil as AudioDeviceID?)
ForEach(audioRouter.outputDevices) { device in
Text(device.displayName).tag(device.id as AudioDeviceID?)
}
}
.pickerStyle(.menu)
}
// Digital Mode Configuration
GroupBox("Digitale Betriebsarten") {
VStack(alignment: .leading, spacing: 8) {
Text("Für FT8, WSPR, RTTY und andere digitale Modi:")
.font(.caption)
.foregroundColor(.secondary)
Button("Für Digimodes konfigurieren") {
_ = audioRouter.configureForDigitalModes()
}
.disabled(!audioRouter.isBlackHoleInstalled)
Toggle("BlackHole verwenden", isOn: $settingsController.useBlackHole)
.disabled(!audioRouter.isBlackHoleInstalled)
}
.padding(.vertical, 4)
}
// Routing Diagram
GroupBox("Routing-Schema") {
VStack(alignment: .leading, spacing: 4) {
Text("FT-991A USB Audio → BlackHole → WSJT-X/fldigi")
.font(.caption.monospaced())
Text("WSJT-X/fldigi → BlackHole → FT-991A USB Audio")
.font(.caption.monospaced())
}
.foregroundColor(.secondary)
.padding(.vertical, 4)
}
// Error display
if let error = audioRouter.lastError {
HStack {
Image(systemName: "exclamationmark.triangle")
.foregroundColor(.orange)
Text(error)
.font(.caption)
}
}
}
.padding()
}
}
}
}
// MARK: - Preview
#Preview {
AudioPanel()
.environmentObject(SettingsController())
.frame(width: 350, height: 500)
}