| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 |
- import Foundation
- import Combine
- /// Shared metering conversion used by both live recording and playback analysis.
- enum AudioLevelNormalizer {
- static let minimumDecibels: Float = -50
- static func normalizedLevel(
- decibels: Float,
- minimumDecibels: Float = AudioLevelNormalizer.minimumDecibels
- ) -> Float {
- guard decibels.isFinite else { return 0 }
- if decibels < minimumDecibels { return 0 }
- if decibels >= 0 { return 1 }
- return min(max((decibels - minimumDecibels) / -minimumDecibels, 0), 1)
- }
- }
- /// 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<URL?, Error>) -> Void)
- func pauseRecording()
- func resumeRecording()
- }
- extension AudioRecorderProtocol {
- var sourceDisplayName: String { "iPhone 麦克风" }
- var statusMessage: String { isRecording ? "正在录音" : "正在准备" }
- var errorMessage: String? { nil }
- func stopRecording(completion: @escaping (Result<URL?, Error>) -> Void) {
- stopRecording()
- completion(.success(outputFileURL))
- }
- }
|