0ecf2c7940
- Add TCP server (VUServer.swift) for external apps to send VU meter values - Server supports VU Protocol (#channel:value), JSON, and raw bytes - Configurable options: port, max clients, remote access, broadcast levels - Add ServerView.swift with full server settings UI and client management - Improve auto-probing to use two-phase detection (port scan then protocol test) - Fix termios c_cc tuple access using withUnsafeMutableBytes - Add network and serial entitlements for server and USB access - Update version to 1.2.0
364 lines
13 KiB
Swift
364 lines
13 KiB
Swift
//
|
|
// ContentView.swift
|
|
// AudioVUMeter
|
|
//
|
|
// Main view containing all VU meters and hardware output
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct ContentView: View {
|
|
@EnvironmentObject var audioEngine: AudioEngine
|
|
@EnvironmentObject var systemMonitor: SystemMonitor
|
|
@EnvironmentObject var serialManager: SerialManager
|
|
@EnvironmentObject var vuServer: VUServer
|
|
|
|
@State private var showSettings = false
|
|
@State private var showHardwareSettings = false
|
|
@State private var showServerSettings = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Background gradient
|
|
LinearGradient(
|
|
gradient: Gradient(colors: [
|
|
Color(red: 0.1, green: 0.1, blue: 0.15),
|
|
Color(red: 0.05, green: 0.05, blue: 0.1)
|
|
]),
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
.ignoresSafeArea()
|
|
|
|
ScrollView {
|
|
VStack(spacing: 16) {
|
|
// Header
|
|
HStack {
|
|
Text("Audio VU Meter")
|
|
.font(.system(size: 24, weight: .bold, design: .rounded))
|
|
.foregroundColor(.white)
|
|
|
|
Spacer()
|
|
|
|
// Server settings button
|
|
Button(action: { showServerSettings.toggle() }) {
|
|
Image(systemName: "server.rack")
|
|
.font(.system(size: 14))
|
|
.foregroundColor(vuServer.isRunning ? .cyan : .gray)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.help("VU Server Settings")
|
|
|
|
// Hardware settings button
|
|
Button(action: { showHardwareSettings.toggle() }) {
|
|
Image(systemName: "cable.connector")
|
|
.font(.system(size: 16))
|
|
.foregroundColor(serialManager.isConnected ? .green : .gray)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.help("Hardware Settings")
|
|
|
|
// Settings button
|
|
Button(action: { showSettings.toggle() }) {
|
|
Image(systemName: "gear")
|
|
.font(.system(size: 18))
|
|
.foregroundColor(.gray)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.popover(isPresented: $showSettings) {
|
|
QuickSettingsView()
|
|
.environmentObject(audioEngine)
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.top, 10)
|
|
|
|
// Audio device info
|
|
HStack {
|
|
Circle()
|
|
.fill(audioEngine.isRunning ? Color.green : Color.red)
|
|
.frame(width: 8, height: 8)
|
|
|
|
Text(audioEngine.selectedDeviceName)
|
|
.font(.system(size: 12, design: .monospaced))
|
|
.foregroundColor(.gray)
|
|
|
|
Spacer()
|
|
|
|
Text(audioEngine.isRunning ? "ACTIVE" : "STOPPED")
|
|
.font(.system(size: 10, weight: .semibold, design: .monospaced))
|
|
.foregroundColor(audioEngine.isRunning ? .green : .red)
|
|
}
|
|
.padding(.horizontal)
|
|
|
|
Divider()
|
|
.background(Color.gray.opacity(0.3))
|
|
|
|
// Audio VU Meters
|
|
VStack(spacing: 15) {
|
|
Text("AUDIO LEVELS")
|
|
.font(.system(size: 11, weight: .semibold, design: .monospaced))
|
|
.foregroundColor(.gray)
|
|
|
|
HStack(spacing: 30) {
|
|
// Left Channel
|
|
VUMeterView(
|
|
level: audioEngine.leftLevel,
|
|
peakLevel: audioEngine.leftPeak,
|
|
label: "L",
|
|
colorScheme: .audio
|
|
)
|
|
|
|
// Right Channel
|
|
VUMeterView(
|
|
level: audioEngine.rightLevel,
|
|
peakLevel: audioEngine.rightPeak,
|
|
label: "R",
|
|
colorScheme: .audio
|
|
)
|
|
}
|
|
|
|
// dB Display
|
|
HStack(spacing: 40) {
|
|
VStack {
|
|
Text(String(format: "%.1f dB", audioEngine.leftLevelDB))
|
|
.font(.system(size: 14, weight: .bold, design: .monospaced))
|
|
.foregroundColor(dbColor(for: audioEngine.leftLevelDB))
|
|
Text("LEFT")
|
|
.font(.system(size: 9, design: .monospaced))
|
|
.foregroundColor(.gray)
|
|
}
|
|
|
|
VStack {
|
|
Text(String(format: "%.1f dB", audioEngine.rightLevelDB))
|
|
.font(.system(size: 14, weight: .bold, design: .monospaced))
|
|
.foregroundColor(dbColor(for: audioEngine.rightLevelDB))
|
|
Text("RIGHT")
|
|
.font(.system(size: 9, design: .monospaced))
|
|
.foregroundColor(.gray)
|
|
}
|
|
}
|
|
}
|
|
.padding()
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.fill(Color.black.opacity(0.3))
|
|
)
|
|
.padding(.horizontal)
|
|
|
|
Divider()
|
|
.background(Color.gray.opacity(0.3))
|
|
|
|
// System Monitors
|
|
VStack(spacing: 15) {
|
|
Text("SYSTEM MONITOR")
|
|
.font(.system(size: 11, weight: .semibold, design: .monospaced))
|
|
.foregroundColor(.gray)
|
|
|
|
HStack(spacing: 25) {
|
|
// CPU Meter
|
|
SystemMeterView(
|
|
value: systemMonitor.cpuUsage,
|
|
label: "CPU",
|
|
unit: "%",
|
|
colorScheme: .cpu
|
|
)
|
|
|
|
// RAM Meter
|
|
SystemMeterView(
|
|
value: systemMonitor.memoryUsage,
|
|
label: "RAM",
|
|
unit: "%",
|
|
colorScheme: .ram
|
|
)
|
|
|
|
// Disk I/O Meter
|
|
SystemMeterView(
|
|
value: systemMonitor.diskActivity,
|
|
label: "DISK",
|
|
unit: "%",
|
|
colorScheme: .disk
|
|
)
|
|
|
|
// Network Meter
|
|
SystemMeterView(
|
|
value: systemMonitor.networkActivity,
|
|
label: "NET",
|
|
unit: "%",
|
|
colorScheme: .network
|
|
)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.fill(Color.black.opacity(0.3))
|
|
)
|
|
.padding(.horizontal)
|
|
|
|
Divider()
|
|
.background(Color.gray.opacity(0.3))
|
|
|
|
// Hardware Output Panel
|
|
HardwarePanelView()
|
|
.environmentObject(serialManager)
|
|
|
|
// VU Server Panel
|
|
ServerPanelView()
|
|
.environmentObject(vuServer)
|
|
|
|
// Control buttons
|
|
HStack(spacing: 15) {
|
|
Button(action: {
|
|
if audioEngine.isRunning {
|
|
audioEngine.stop()
|
|
} else {
|
|
audioEngine.start()
|
|
}
|
|
}) {
|
|
HStack {
|
|
Image(systemName: audioEngine.isRunning ? "stop.fill" : "play.fill")
|
|
Text(audioEngine.isRunning ? "Stop" : "Start")
|
|
}
|
|
.frame(width: 80)
|
|
}
|
|
.buttonStyle(ControlButtonStyle(color: audioEngine.isRunning ? .red : .green))
|
|
|
|
Button(action: {
|
|
audioEngine.resetPeaks()
|
|
}) {
|
|
HStack {
|
|
Image(systemName: "arrow.counterclockwise")
|
|
Text("Reset")
|
|
}
|
|
.frame(width: 80)
|
|
}
|
|
.buttonStyle(ControlButtonStyle(color: .orange))
|
|
}
|
|
.padding(.bottom, 15)
|
|
}
|
|
}
|
|
}
|
|
.frame(width: 400, height: 880)
|
|
.sheet(isPresented: $showHardwareSettings) {
|
|
HardwareSettingsSheet()
|
|
.environmentObject(serialManager)
|
|
}
|
|
.sheet(isPresented: $showServerSettings) {
|
|
ServerSettingsSheet()
|
|
.environmentObject(vuServer)
|
|
}
|
|
.onAppear {
|
|
audioEngine.start()
|
|
systemMonitor.startMonitoring()
|
|
}
|
|
.onDisappear {
|
|
audioEngine.stop()
|
|
systemMonitor.stopMonitoring()
|
|
serialManager.disconnect()
|
|
}
|
|
}
|
|
|
|
private func dbColor(for db: Double) -> Color {
|
|
if db > -3 { return .red }
|
|
if db > -10 { return .orange }
|
|
if db > -20 { return .yellow }
|
|
return .green
|
|
}
|
|
}
|
|
|
|
// MARK: - Hardware Settings Sheet
|
|
struct HardwareSettingsSheet: View {
|
|
@EnvironmentObject var serialManager: SerialManager
|
|
@Environment(\.dismiss) var dismiss
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Header
|
|
HStack {
|
|
Text("Hardware Configuration")
|
|
.font(.headline)
|
|
Spacer()
|
|
Button("Done") { dismiss() }
|
|
}
|
|
.padding()
|
|
.background(Color(nsColor: .windowBackgroundColor))
|
|
|
|
Divider()
|
|
|
|
// Settings content
|
|
HardwareSettingsView()
|
|
.environmentObject(serialManager)
|
|
}
|
|
.frame(width: 500, height: 600)
|
|
}
|
|
}
|
|
|
|
// MARK: - Quick Settings Popover
|
|
struct QuickSettingsView: View {
|
|
@EnvironmentObject var audioEngine: AudioEngine
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 15) {
|
|
Text("Audio Device")
|
|
.font(.headline)
|
|
|
|
Picker("Device", selection: $audioEngine.selectedDeviceID) {
|
|
ForEach(audioEngine.availableDevices, id: \.id) { device in
|
|
Text(device.name).tag(device.id)
|
|
}
|
|
}
|
|
.labelsHidden()
|
|
.frame(width: 250)
|
|
.onChange(of: audioEngine.selectedDeviceID) { _ in
|
|
audioEngine.switchDevice()
|
|
}
|
|
|
|
Divider()
|
|
|
|
Text("Reference Level")
|
|
.font(.headline)
|
|
|
|
HStack {
|
|
Text("-60 dB")
|
|
.font(.caption)
|
|
Slider(value: $audioEngine.referenceLevel, in: -60...0)
|
|
Text("0 dB")
|
|
.font(.caption)
|
|
}
|
|
|
|
Text("Peak Hold Time: \(Int(audioEngine.peakHoldTime))s")
|
|
.font(.caption)
|
|
|
|
Slider(value: $audioEngine.peakHoldTime, in: 0.5...5.0)
|
|
}
|
|
.padding()
|
|
.frame(width: 300)
|
|
}
|
|
}
|
|
|
|
// MARK: - Control Button Style
|
|
struct ControlButtonStyle: ButtonStyle {
|
|
let color: Color
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
configuration.label
|
|
.font(.system(size: 12, weight: .semibold))
|
|
.foregroundColor(.white)
|
|
.padding(.horizontal, 15)
|
|
.padding(.vertical, 8)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.fill(color.opacity(configuration.isPressed ? 0.6 : 0.8))
|
|
)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
ContentView()
|
|
.environmentObject(AudioEngine())
|
|
.environmentObject(SystemMonitor())
|
|
.environmentObject(SerialManager())
|
|
.environmentObject(VUServer())
|
|
}
|