import Foundation import Combine /// The physical source selected for one recording segment. enum RecordingSourceChoice: Identifiable, Equatable { case iPhone case spark(deviceID: String, displayName: String) var id: String { switch self { case .iPhone: return "iphone" case .spark(let deviceID, _): return "spark-\(deviceID)" } } var displayName: String { switch self { case .iPhone: return "iPhone 麦克风" case .spark(_, let displayName): return displayName } } var systemImage: String { switch self { case .iPhone: return "iphone" case .spark: return "wave.3.right.circle.fill" } } } enum AudioRecorderError: LocalizedError { case failedToStart(String) case failedToStop(String) var errorDescription: String? { switch self { case .failedToStart(let message), .failedToStop(let message): return message } } } /// Protocol abstracting audio recording capabilities. protocol AudioRecorderProtocol: ObservableObject { /// Current recording state var isRecording: Bool { get } /// Elapsed recording time in seconds var elapsedTime: TimeInterval { get } /// Current amplitude level (0.0 - 1.0) for waveform visualization var currentAmplitude: Float { get } /// Rolling buffer of recent amplitude samples for waveform drawing var waveformSamples: [Float] { get } /// The local file URL of the current/last recording. var outputFileURL: URL? { get } /// Whether the active recording is paused var isPaused: Bool { get } /// User-facing name of the active recording source. var sourceDisplayName: String { get } /// Current source status, such as connecting or recording. var statusMessage: String { get } /// A recoverable error that should be shown in the recording UI. var errorMessage: String? { get } func startRecording() func stopRecording() func stopRecording(completion: @escaping (Result) -> Void) func pauseRecording() func resumeRecording() } extension AudioRecorderProtocol { var sourceDisplayName: String { "iPhone 麦克风" } var statusMessage: String { isRecording ? "正在录音" : "正在准备" } var errorMessage: String? { nil } func stopRecording(completion: @escaping (Result) -> Void) { stopRecording() completion(.success(outputFileURL)) } }