196 lines
7.1 KiB
Swift
196 lines
7.1 KiB
Swift
import Foundation
|
|
import AVFoundation
|
|
import SwiftUI
|
|
|
|
class CustomSoundManager: ObservableObject {
|
|
static let shared = CustomSoundManager()
|
|
|
|
enum SoundType: String {
|
|
case start
|
|
case stop
|
|
|
|
var isUsingKey: String { "isUsingCustom\(rawValue.capitalized)Sound" }
|
|
var filenameKey: String { "custom\(rawValue.capitalized)SoundFilename" }
|
|
var standardName: String { "Custom\(rawValue.capitalized)Sound" }
|
|
}
|
|
|
|
@Published var isUsingCustomStartSound: Bool {
|
|
didSet { UserDefaults.standard.set(isUsingCustomStartSound, forKey: SoundType.start.isUsingKey) }
|
|
}
|
|
|
|
@Published var isUsingCustomStopSound: Bool {
|
|
didSet { UserDefaults.standard.set(isUsingCustomStopSound, forKey: SoundType.stop.isUsingKey) }
|
|
}
|
|
|
|
private let maxSoundDuration: TimeInterval = 3.0
|
|
|
|
private var customStartSoundFilename: String? {
|
|
didSet { updateFilenameInUserDefaults(filename: customStartSoundFilename, for: .start) }
|
|
}
|
|
|
|
private var customStopSoundFilename: String? {
|
|
didSet { updateFilenameInUserDefaults(filename: customStopSoundFilename, for: .stop) }
|
|
}
|
|
|
|
private func updateFilenameInUserDefaults(filename: String?, for type: SoundType) {
|
|
if let filename = filename {
|
|
UserDefaults.standard.set(filename, forKey: type.filenameKey)
|
|
} else {
|
|
UserDefaults.standard.removeObject(forKey: type.filenameKey)
|
|
}
|
|
}
|
|
|
|
private init() {
|
|
self.isUsingCustomStartSound = UserDefaults.standard.bool(forKey: SoundType.start.isUsingKey)
|
|
self.isUsingCustomStopSound = UserDefaults.standard.bool(forKey: SoundType.stop.isUsingKey)
|
|
self.customStartSoundFilename = UserDefaults.standard.string(forKey: SoundType.start.filenameKey)
|
|
self.customStopSoundFilename = UserDefaults.standard.string(forKey: SoundType.stop.filenameKey)
|
|
|
|
createCustomSoundsDirectoryIfNeeded()
|
|
}
|
|
|
|
private func customSoundsDirectory() -> URL? {
|
|
guard let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
|
|
return nil
|
|
}
|
|
return appSupport.appendingPathComponent("VoiceInk/CustomSounds")
|
|
}
|
|
|
|
private func createCustomSoundsDirectoryIfNeeded() {
|
|
guard let directory = customSoundsDirectory() else { return }
|
|
|
|
if !FileManager.default.fileExists(atPath: directory.path) {
|
|
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
}
|
|
}
|
|
|
|
func getCustomSoundURL(for type: SoundType) -> URL? {
|
|
let isUsing = (type == .start) ? isUsingCustomStartSound : isUsingCustomStopSound
|
|
let filename = (type == .start) ? customStartSoundFilename : customStopSoundFilename
|
|
|
|
guard isUsing, let filename = filename, let directory = customSoundsDirectory() else {
|
|
return nil
|
|
}
|
|
return directory.appendingPathComponent(filename)
|
|
}
|
|
|
|
func setCustomSound(url: URL, for type: SoundType) -> Result<Void, CustomSoundError> {
|
|
let result = validateAudioFile(url: url)
|
|
switch result {
|
|
case .success:
|
|
let copyResult = copySoundFile(from: url, standardName: type.standardName)
|
|
switch copyResult {
|
|
case .success(let filename):
|
|
if type == .start {
|
|
customStartSoundFilename = filename
|
|
isUsingCustomStartSound = true
|
|
} else {
|
|
customStopSoundFilename = filename
|
|
isUsingCustomStopSound = true
|
|
}
|
|
notifyCustomSoundsChanged()
|
|
return .success(())
|
|
case .failure(let error):
|
|
return .failure(error)
|
|
}
|
|
case .failure(let error):
|
|
return .failure(error)
|
|
}
|
|
}
|
|
|
|
func resetSoundToDefault(for type: SoundType) {
|
|
let filename = (type == .start) ? customStartSoundFilename : customStopSoundFilename
|
|
|
|
if let filename = filename, let directory = customSoundsDirectory() {
|
|
let fileURL = directory.appendingPathComponent(filename)
|
|
try? FileManager.default.removeItem(at: fileURL)
|
|
}
|
|
|
|
if type == .start {
|
|
isUsingCustomStartSound = false
|
|
customStartSoundFilename = nil
|
|
} else {
|
|
isUsingCustomStopSound = false
|
|
customStopSoundFilename = nil
|
|
}
|
|
notifyCustomSoundsChanged()
|
|
}
|
|
|
|
private func notifyCustomSoundsChanged() {
|
|
NotificationCenter.default.post(name: NSNotification.Name("CustomSoundsChanged"), object: nil)
|
|
}
|
|
|
|
func getSoundDisplayName(for type: SoundType) -> String? {
|
|
return (type == .start) ? customStartSoundFilename : customStopSoundFilename
|
|
}
|
|
|
|
private func copySoundFile(from sourceURL: URL, standardName: String) -> Result<String, CustomSoundError> {
|
|
guard let directory = customSoundsDirectory() else {
|
|
return .failure(.directoryCreationFailed)
|
|
}
|
|
|
|
let fileExtension = sourceURL.pathExtension
|
|
let newFilename = "\(standardName).\(fileExtension)"
|
|
let destinationURL = directory.appendingPathComponent(newFilename)
|
|
|
|
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
|
try? FileManager.default.removeItem(at: destinationURL)
|
|
}
|
|
|
|
do {
|
|
try FileManager.default.copyItem(at: sourceURL, to: destinationURL)
|
|
return .success(newFilename)
|
|
} catch {
|
|
return .failure(.fileCopyFailed)
|
|
}
|
|
}
|
|
|
|
private func validateAudioFile(url: URL) -> Result<Void, CustomSoundError> {
|
|
guard FileManager.default.fileExists(atPath: url.path) else {
|
|
return .failure(.fileNotFound)
|
|
}
|
|
|
|
let asset = AVAsset(url: url)
|
|
let duration = asset.duration.seconds
|
|
|
|
guard duration.isFinite && duration > 0 else {
|
|
return .failure(.invalidAudioFile)
|
|
}
|
|
|
|
if duration > maxSoundDuration {
|
|
return .failure(.durationTooLong(duration: duration, maxDuration: maxSoundDuration))
|
|
}
|
|
|
|
do {
|
|
_ = try AVAudioPlayer(contentsOf: url)
|
|
} catch {
|
|
return .failure(.invalidAudioFile)
|
|
}
|
|
|
|
return .success(())
|
|
}
|
|
}
|
|
|
|
enum CustomSoundError: LocalizedError {
|
|
case fileNotFound
|
|
case invalidAudioFile
|
|
case durationTooLong(duration: TimeInterval, maxDuration: TimeInterval)
|
|
case directoryCreationFailed
|
|
case fileCopyFailed
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .fileNotFound:
|
|
return "Audio file not found"
|
|
case .invalidAudioFile:
|
|
return "Invalid audio file format"
|
|
case .durationTooLong(let duration, let maxDuration):
|
|
return String(format: "Audio file is %.1f seconds long. Please use an audio file that is %.0f seconds or shorter for start and stop sounds.", duration, maxDuration)
|
|
case .directoryCreationFailed:
|
|
return "Failed to create custom sounds directory"
|
|
case .fileCopyFailed:
|
|
return "Failed to copy audio file"
|
|
}
|
|
}
|
|
}
|