From b59fe7fe26843cf94cea9d42e217ed17098aabf2 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 8 Sep 2025 16:29:47 -0500 Subject: [PATCH] Signed-off-by: Matt Bruce --- .gitignore | 137 +++++++++++++++ AudioPlaybackKit/Package.swift | 32 ++++ AudioPlaybackKit/README.md | 158 ++++++++++++++++++ .../Constants/AudioConstants.swift | 33 ++++ .../AudioPlaybackKit/Models/Sound.swift | 33 ++++ .../Models/SoundConfiguration.swift | 112 ++++++++----- .../Services/NoisePlayer.swift | 34 ++-- .../Services/WakeLockService.swift | 19 ++- .../ViewModels/NoiseViewModel.swift | 31 ++-- .../AudioPlaybackKitTests.swift | 48 ++++++ TheNoiseClock.xcodeproj/project.pbxproj | 24 +++ .../xcschemes/xcschememanagement.plist | 2 +- .../Core/Constants/AudioConstants.swift | 33 ---- TheNoiseClock/Models/Sound.swift | 33 ---- TheNoiseClock/ViewModels/ClockViewModel.swift | 1 + TheNoiseClock/Views/Alarms/AddAlarmView.swift | 1 + .../Components/SoundSelectionView.swift | 1 + .../Views/Alarms/EditAlarmView.swift | 1 + .../Noise/Components/SoundCategoryView.swift | 1 + .../Noise/Components/SoundControlView.swift | 1 + TheNoiseClock/Views/Noise/NoiseView.swift | 1 + 21 files changed, 585 insertions(+), 151 deletions(-) create mode 100644 .gitignore create mode 100644 AudioPlaybackKit/Package.swift create mode 100644 AudioPlaybackKit/README.md create mode 100644 AudioPlaybackKit/Sources/AudioPlaybackKit/Constants/AudioConstants.swift create mode 100644 AudioPlaybackKit/Sources/AudioPlaybackKit/Models/Sound.swift rename {TheNoiseClock => AudioPlaybackKit/Sources/AudioPlaybackKit}/Models/SoundConfiguration.swift (55%) rename {TheNoiseClock => AudioPlaybackKit/Sources/AudioPlaybackKit}/Services/NoisePlayer.swift (91%) rename {TheNoiseClock => AudioPlaybackKit/Sources/AudioPlaybackKit}/Services/WakeLockService.swift (84%) rename {TheNoiseClock => AudioPlaybackKit/Sources/AudioPlaybackKit}/ViewModels/NoiseViewModel.swift (60%) create mode 100644 AudioPlaybackKit/Tests/AudioPlaybackKitTests/AudioPlaybackKitTests.swift delete mode 100644 TheNoiseClock/Core/Constants/AudioConstants.swift delete mode 100644 TheNoiseClock/Models/Sound.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4eec202 --- /dev/null +++ b/.gitignore @@ -0,0 +1,137 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# IDEs +.vscode/ +.idea/ + +# Temporary files +*.tmp +*.temp +*~ + +# Logs +*.log + +# AudioPlaybackKit specific +# Keep the package source but ignore build artifacts +AudioPlaybackKit/.build/ +AudioPlaybackKit/Package.resolved + +# Test results +*.xcresult diff --git a/AudioPlaybackKit/Package.swift b/AudioPlaybackKit/Package.swift new file mode 100644 index 0000000..83ffd95 --- /dev/null +++ b/AudioPlaybackKit/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "AudioPlaybackKit", + platforms: [ + .iOS(.v17) + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "AudioPlaybackKit", + targets: ["AudioPlaybackKit"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "AudioPlaybackKit", + dependencies: [] + ), + .testTarget( + name: "AudioPlaybackKitTests", + dependencies: ["AudioPlaybackKit"]), + ] +) diff --git a/AudioPlaybackKit/README.md b/AudioPlaybackKit/README.md new file mode 100644 index 0000000..71bcdd3 --- /dev/null +++ b/AudioPlaybackKit/README.md @@ -0,0 +1,158 @@ +# AudioPlaybackKit + +A Swift package for audio playback functionality, specifically designed for white noise and ambient sound applications. + +## Features + +- **Audio Playback**: High-quality audio playback with background support +- **Sound Management**: Load and manage audio files from bundles +- **Wake Lock**: Prevent device from sleeping during audio playback +- **Interruption Handling**: Automatic handling of audio interruptions (calls, etc.) +- **Configuration**: JSON-based sound configuration system +- **Preview Support**: Built-in audio preview functionality + +## Requirements + +- iOS 16.0+ / macOS 13.0+ +- Swift 5.9+ + +## Installation + +### Swift Package Manager + +Add the following to your `Package.swift` file: + +```swift +dependencies: [ + .package(url: "https://github.com/yourusername/AudioPlaybackKit.git", from: "1.0.0") +] +``` + +Or add it through Xcode: +1. File → Add Package Dependencies +2. Enter the repository URL +3. Select the version and add to your target + +## Usage + +### Basic Audio Playback + +```swift +import AudioPlaybackKit + +// Get the shared player instance +let player = NoisePlayer.shared + +// Create a sound +let sound = Sound( + name: "White Noise", + fileName: "white-noise.mp3", + category: "ambient", + description: "Classic white noise" +) + +// Play the sound +player.playSound(sound) + +// Stop the sound +player.stopSound() +``` + +### Using the ViewModel + +```swift +import AudioPlaybackKit + +// Create a view model +let viewModel = NoiseViewModel() + +// Get available sounds +let sounds = viewModel.availableSounds + +// Play a sound +viewModel.playSound(sounds.first!) + +// Preview a sound (3 seconds) +viewModel.previewSound(sounds.first!) + +// Stop playback +viewModel.stopSound() +``` + +### Sound Configuration + +Create a `sounds.json` file in your app bundle: + +```json +{ + "sounds": [ + { + "id": "white-noise", + "name": "White Noise", + "fileName": "white-noise.mp3", + "category": "ambient", + "description": "Classic white noise for focus and relaxation", + "bundleName": "Ambient" + } + ], + "categories": [ + { + "id": "ambient", + "name": "Ambient", + "description": "General ambient sounds", + "bundleName": "Ambient" + } + ], + "settings": { + "defaultVolume": 0.8, + "defaultLoopCount": -1, + "preloadSounds": true, + "preloadStrategy": "category", + "audioSessionCategory": "playback", + "audioSessionMode": "default", + "audioSessionOptions": ["mixWithOthers"] + } +} +``` + +### Wake Lock Service + +```swift +import AudioPlaybackKit + +let wakeLock = WakeLockService.shared + +// Enable wake lock (prevents device from sleeping) +wakeLock.enableWakeLock() + +// Disable wake lock +wakeLock.disableWakeLock() + +// Check if active +if wakeLock.isActive { + print("Wake lock is active") +} +``` + +## Architecture + +### Core Components + +- **NoisePlayer**: Main audio playback service +- **SoundConfigurationService**: Manages sound configuration and loading +- **WakeLockService**: Prevents device from sleeping during playback +- **NoiseViewModel**: SwiftUI-friendly view model for audio playback + +### Models + +- **Sound**: Represents an audio file with metadata +- **SoundConfiguration**: JSON configuration structure +- **AudioSettings**: Audio session and playback settings + +## License + +This package is available under the MIT license. See the LICENSE file for more info. + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/Constants/AudioConstants.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Constants/AudioConstants.swift new file mode 100644 index 0000000..d687a55 --- /dev/null +++ b/AudioPlaybackKit/Sources/AudioPlaybackKit/Constants/AudioConstants.swift @@ -0,0 +1,33 @@ +// +// AudioConstants.swift +// AudioPlaybackKit +// +// Created by Matt Bruce on 9/8/25. +// + +import Foundation +import AVFAudio + +/// Audio-related constants and configuration +public enum AudioConstants { + + // MARK: - Audio Session Configuration + public enum AudioSession { + public static let category = AVAudioSession.Category.playback + public static let mode = AVAudioSession.Mode.default + public static let options: AVAudioSession.CategoryOptions = [.mixWithOthers] + } + + // MARK: - Playback Settings + public enum Playback { + public static let numberOfLoops = -1 // Infinite loop + public static let prepareToPlay = true + } + + // MARK: - Volume + public enum Volume { + public static let min: Float = 0.0 + public static let max: Float = 1.0 + public static let `default`: Float = 0.8 + } +} diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/Sound.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/Sound.swift new file mode 100644 index 0000000..05fe3d7 --- /dev/null +++ b/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/Sound.swift @@ -0,0 +1,33 @@ +// +// Sound.swift +// AudioPlaybackKit +// +// Created by Matt Bruce on 9/8/25. +// + +import Foundation + +/// Sound data model for audio files +public struct Sound: Identifiable, Hashable { + public let id: String + public let name: String + public let fileName: String + public let category: String + public let description: String + public let bundleName: String? // Optional bundle name for organization + + // MARK: - Initialization + public init(name: String, fileName: String, category: String, description: String, bundleName: String? = nil) { + self.id = fileName // Use fileName as stable identifier + self.name = name + self.fileName = fileName + self.category = category + self.description = description + self.bundleName = bundleName + } + + // MARK: - Hashable + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/TheNoiseClock/Models/SoundConfiguration.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/SoundConfiguration.swift similarity index 55% rename from TheNoiseClock/Models/SoundConfiguration.swift rename to AudioPlaybackKit/Sources/AudioPlaybackKit/Models/SoundConfiguration.swift index 03b88ff..b9dc258 100644 --- a/TheNoiseClock/Models/SoundConfiguration.swift +++ b/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/SoundConfiguration.swift @@ -1,65 +1,97 @@ // // SoundConfiguration.swift -// TheNoiseClock +// AudioPlaybackKit // -// Created by Matt Bruce on 9/7/25. +// Created by Matt Bruce on 9/8/25. // import Foundation /// Configuration model for sound system loaded from JSON -struct SoundConfiguration: Codable { - let sounds: [SoundConfig] - let categories: [SoundCategory] - let settings: AudioSettings +public struct SoundConfiguration: Codable { + public let sounds: [SoundConfig] + public let categories: [SoundCategory] + public let settings: AudioSettings + + public init(sounds: [SoundConfig], categories: [SoundCategory], settings: AudioSettings) { + self.sounds = sounds + self.categories = categories + self.settings = settings + } } /// Individual sound configuration -struct SoundConfig: Codable, Identifiable { - let id: String - let name: String - let fileName: String - let category: String - let description: String - let bundleName: String? // Optional bundle name for organization +public struct SoundConfig: Codable, Identifiable { + public let id: String + public let name: String + public let fileName: String + public let category: String + public let description: String + public let bundleName: String? // Optional bundle name for organization + + public init(id: String, name: String, fileName: String, category: String, description: String, bundleName: String? = nil) { + self.id = id + self.name = name + self.fileName = fileName + self.category = category + self.description = description + self.bundleName = bundleName + } /// Convert to Sound model for compatibility - func toSound() -> Sound { + public func toSound() -> Sound { return Sound(name: name, fileName: fileName, category: category, description: description, bundleName: bundleName) } } /// Sound category configuration -struct SoundCategory: Codable, Identifiable { - let id: String - let name: String - let description: String - let bundleName: String? // Optional bundle name for this category +public struct SoundCategory: Codable, Identifiable { + public let id: String + public let name: String + public let description: String + public let bundleName: String? // Optional bundle name for this category + + public init(id: String, name: String, description: String, bundleName: String? = nil) { + self.id = id + self.name = name + self.description = description + self.bundleName = bundleName + } } /// Audio settings configuration -struct AudioSettings: Codable { - let defaultVolume: Float - let defaultLoopCount: Int - let preloadSounds: Bool - let preloadStrategy: String // "all", "category", "none" - let audioSessionCategory: String - let audioSessionMode: String - let audioSessionOptions: [String] +public struct AudioSettings: Codable { + public let defaultVolume: Float + public let defaultLoopCount: Int + public let preloadSounds: Bool + public let preloadStrategy: String // "all", "category", "none" + public let audioSessionCategory: String + public let audioSessionMode: String + public let audioSessionOptions: [String] + + public init(defaultVolume: Float, defaultLoopCount: Int, preloadSounds: Bool, preloadStrategy: String, audioSessionCategory: String, audioSessionMode: String, audioSessionOptions: [String]) { + self.defaultVolume = defaultVolume + self.defaultLoopCount = defaultLoopCount + self.preloadSounds = preloadSounds + self.preloadStrategy = preloadStrategy + self.audioSessionCategory = audioSessionCategory + self.audioSessionMode = audioSessionMode + self.audioSessionOptions = audioSessionOptions + } } /// Service for loading and managing sound configuration -class SoundConfigurationService { - static let shared = SoundConfigurationService() +public class SoundConfigurationService { + public static let shared = SoundConfigurationService() private var configuration: SoundConfiguration? private init() {} /// Load sound configuration from JSON file - func loadConfiguration() -> SoundConfiguration? { - guard let url = Bundle.main.url(forResource: "sounds", withExtension: "json") else { - print("❌ sounds.json not found in bundle") + public func loadConfiguration(from bundle: Bundle = .main, fileName: String = "sounds") -> SoundConfiguration? { + guard let url = bundle.url(forResource: fileName, withExtension: "json") else { + print("❌ \(fileName).json not found in bundle") return nil } @@ -76,7 +108,7 @@ class SoundConfigurationService { } /// Get current configuration - func getConfiguration() -> SoundConfiguration? { + public func getConfiguration() -> SoundConfiguration? { if configuration == nil { return loadConfiguration() } @@ -84,7 +116,7 @@ class SoundConfigurationService { } /// Get all available sounds - func getAvailableSounds() -> [Sound] { + public func getAvailableSounds() -> [Sound] { guard let config = getConfiguration() else { print("⚠️ No configuration available, falling back to constants") return getFallbackSounds() @@ -94,7 +126,7 @@ class SoundConfigurationService { } /// Get sounds by category - func getSoundsByCategory(_ categoryId: String) -> [Sound] { + public func getSoundsByCategory(_ categoryId: String) -> [Sound] { guard let config = getConfiguration() else { return [] } @@ -105,7 +137,7 @@ class SoundConfigurationService { } /// Get sounds by bundle name - func getSoundsByBundle(_ bundleName: String) -> [Sound] { + public func getSoundsByBundle(_ bundleName: String) -> [Sound] { guard let config = getConfiguration() else { return [] } @@ -116,17 +148,17 @@ class SoundConfigurationService { } /// Get alarm sounds specifically - func getAlarmSounds() -> [Sound] { + public func getAlarmSounds() -> [Sound] { return getSoundsByCategory("alarm") } /// Get available categories - func getAvailableCategories() -> [SoundCategory] { + public func getAvailableCategories() -> [SoundCategory] { return getConfiguration()?.categories ?? [] } /// Get audio settings - func getAudioSettings() -> AudioSettings? { + public func getAudioSettings() -> AudioSettings? { return getConfiguration()?.settings } @@ -147,4 +179,4 @@ class SoundConfigurationService { Sound(name: "Voice Wake Up", fileName: "voice-wakeup.mp3", category: "alarm", description: "Voice-based wake up sound", bundleName: "AlarmSounds") ] } -} \ No newline at end of file +} diff --git a/TheNoiseClock/Services/NoisePlayer.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/NoisePlayer.swift similarity index 91% rename from TheNoiseClock/Services/NoisePlayer.swift rename to AudioPlaybackKit/Sources/AudioPlaybackKit/Services/NoisePlayer.swift index af556e7..5007122 100644 --- a/TheNoiseClock/Services/NoisePlayer.swift +++ b/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/NoisePlayer.swift @@ -1,19 +1,20 @@ // // NoisePlayer.swift -// TheNoiseClock +// AudioPlaybackKit // -// Created by Matt Bruce on 9/7/25. +// Created by Matt Bruce on 9/8/25. // import AVFoundation import Observation /// Audio playback service for white noise and ambient sounds +@available(iOS 17.0, *) @Observable -class NoisePlayer { +public class NoisePlayer { // MARK: - Singleton - static let shared = NoisePlayer() + public static let shared = NoisePlayer() // MARK: - Properties private var players: [String: AVAudioPlayer] = [:] @@ -21,14 +22,13 @@ class NoisePlayer { private var currentSound: Sound? private var shouldResumeAfterInterruption = false private let wakeLockService = WakeLockService.shared - private let focusModeService = FocusModeService.shared + private let soundConfigurationService = SoundConfigurationService.shared // MARK: - Initialization private init() { setupAudioSession() preloadSounds() setupAudioInterruptionHandling() - focusModeService.configureForFocusModes() } deinit { @@ -36,21 +36,13 @@ class NoisePlayer { } // MARK: - Public Interface - var isPlaying: Bool { + public var isPlaying: Bool { return currentPlayer?.isPlaying ?? false } - func playSound(_ sound: Sound) { + public func playSound(_ sound: Sound) { print("🎵 Attempting to play: \(sound.name)") - // Check Focus mode status if respecting Focus modes - Task { - let notificationsAllowed = await focusModeService.areNotificationsAllowed() - if !notificationsAllowed { - print("🎯 Focus mode is active - audio playback may be limited") - } - } - // Stop current sound if playing stopSound() @@ -96,7 +88,7 @@ class NoisePlayer { } } - func stopSound() { + public func stopSound() { currentPlayer?.stop() currentPlayer = nil currentSound = nil @@ -133,7 +125,7 @@ class NoisePlayer { private func setupAudioSession() { do { - let settings = SoundConfigurationService.shared.getAudioSettings() + let settings = soundConfigurationService.getAudioSettings() // Use configuration settings or fall back to constants let category = settings?.audioSessionCategory == "playback" ? @@ -147,7 +139,7 @@ class NoisePlayer { try AVAudioSession.sharedInstance().setActive(true) // Configure for background audio playback - try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers, .allowBluetoothHFP, .allowBluetoothA2DP]) + try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers]) try AVAudioSession.sharedInstance().setActive(true) print("🔊 Audio session configured for background playback") @@ -160,8 +152,8 @@ class NoisePlayer { print("📁 Preloading audio files...") // Get sound configuration - let sounds = SoundConfigurationService.shared.getAvailableSounds() - let settings = SoundConfigurationService.shared.getAudioSettings() + let sounds = soundConfigurationService.getAvailableSounds() + let settings = soundConfigurationService.getAudioSettings() for sound in sounds { guard let fileUrl = getURL(for: sound) else { diff --git a/TheNoiseClock/Services/WakeLockService.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/WakeLockService.swift similarity index 84% rename from TheNoiseClock/Services/WakeLockService.swift rename to AudioPlaybackKit/Sources/AudioPlaybackKit/Services/WakeLockService.swift index b0da6a3..c6b13cb 100644 --- a/TheNoiseClock/Services/WakeLockService.swift +++ b/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/WakeLockService.swift @@ -1,22 +1,23 @@ // // WakeLockService.swift -// TheNoiseClock +// AudioPlaybackKit // -// Created by Matt Bruce on 9/7/25. +// Created by Matt Bruce on 9/8/25. // import UIKit import Observation /// Service to manage screen wake lock and prevent device from sleeping +@available(iOS 17.0, *) @Observable -class WakeLockService { +public class WakeLockService { // MARK: - Singleton - static let shared = WakeLockService() + public static let shared = WakeLockService() // MARK: - Properties - private(set) var isWakeLockActive = false + public private(set) var isWakeLockActive = false private var wakeLockTimer: Timer? // MARK: - Initialization @@ -29,7 +30,7 @@ class WakeLockService { // MARK: - Public Interface /// Enable wake lock to prevent device from sleeping - func enableWakeLock() { + public func enableWakeLock() { guard !isWakeLockActive else { return } // Prevent device from sleeping @@ -49,7 +50,7 @@ class WakeLockService { } /// Disable wake lock and allow device to sleep normally - func disableWakeLock() { + public func disableWakeLock() { guard isWakeLockActive else { return } // Allow device to sleep normally @@ -64,7 +65,7 @@ class WakeLockService { } /// Toggle wake lock state - func toggleWakeLock() { + public func toggleWakeLock() { if isWakeLockActive { disableWakeLock() } else { @@ -73,7 +74,7 @@ class WakeLockService { } /// Check if wake lock is currently active - var isActive: Bool { + public var isActive: Bool { return isWakeLockActive } } diff --git a/TheNoiseClock/ViewModels/NoiseViewModel.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/ViewModels/NoiseViewModel.swift similarity index 60% rename from TheNoiseClock/ViewModels/NoiseViewModel.swift rename to AudioPlaybackKit/Sources/AudioPlaybackKit/ViewModels/NoiseViewModel.swift index ceea79c..907faee 100644 --- a/TheNoiseClock/ViewModels/NoiseViewModel.swift +++ b/AudioPlaybackKit/Sources/AudioPlaybackKit/ViewModels/NoiseViewModel.swift @@ -1,45 +1,48 @@ // // NoiseViewModel.swift -// TheNoiseClock +// AudioPlaybackKit // -// Created by Matt Bruce on 9/7/25. +// Created by Matt Bruce on 9/8/25. // import Foundation import Observation /// ViewModel for noise/audio playback +@available(iOS 17.0, *) @Observable -class NoiseViewModel { +public class NoiseViewModel { // MARK: - Properties private let noisePlayer: NoisePlayer - var isPreviewing: Bool = false - var previewSound: Sound? + private let soundConfigurationService: SoundConfigurationService + public var isPreviewing: Bool = false + public var previewSound: Sound? - var isPlaying: Bool { + public var isPlaying: Bool { noisePlayer.isPlaying } - var availableSounds: [Sound] { - return SoundConfigurationService.shared.getAvailableSounds() + public var availableSounds: [Sound] { + return soundConfigurationService.getAvailableSounds() } // MARK: - Initialization - init(noisePlayer: NoisePlayer = NoisePlayer.shared) { + public init(noisePlayer: NoisePlayer = NoisePlayer.shared, soundConfigurationService: SoundConfigurationService = SoundConfigurationService.shared) { self.noisePlayer = noisePlayer + self.soundConfigurationService = soundConfigurationService } // MARK: - Public Interface - func playSound(_ sound: Sound) { + public func playSound(_ sound: Sound) { noisePlayer.playSound(sound) } - func stopSound() { + public func stopSound() { noisePlayer.stopSound() } - func selectSound(_ sound: Sound) { + public func selectSound(_ sound: Sound) { // Stop any current playback when selecting a new sound if isPlaying { stopSound() @@ -49,7 +52,7 @@ class NoiseViewModel { } // MARK: - Preview Functionality - func previewSound(_ sound: Sound) { + public func previewSound(_ sound: Sound) { // Stop any current preview stopPreview() @@ -66,7 +69,7 @@ class NoiseViewModel { } } - func stopPreview() { + public func stopPreview() { if isPreviewing { noisePlayer.stopSound() isPreviewing = false diff --git a/AudioPlaybackKit/Tests/AudioPlaybackKitTests/AudioPlaybackKitTests.swift b/AudioPlaybackKit/Tests/AudioPlaybackKitTests/AudioPlaybackKitTests.swift new file mode 100644 index 0000000..8411a81 --- /dev/null +++ b/AudioPlaybackKit/Tests/AudioPlaybackKitTests/AudioPlaybackKitTests.swift @@ -0,0 +1,48 @@ +// +// AudioPlaybackKitTests.swift +// AudioPlaybackKitTests +// +// Created by Matt Bruce on 9/8/25. +// + +import XCTest +@testable import AudioPlaybackKit + +final class AudioPlaybackKitTests: XCTestCase { + + func testSoundModel() throws { + let sound = Sound( + name: "Test Sound", + fileName: "test.mp3", + category: "test", + description: "Test description" + ) + + XCTAssertEqual(sound.name, "Test Sound") + XCTAssertEqual(sound.fileName, "test.mp3") + XCTAssertEqual(sound.category, "test") + } + + func testSoundConfigurationService() throws { + let service = SoundConfigurationService.shared + let sounds = service.getAvailableSounds() + + // Should have fallback sounds if no configuration is loaded + XCTAssertFalse(sounds.isEmpty) + } + + func testWakeLockService() throws { + let service = WakeLockService.shared + + // Initially should not be active + XCTAssertFalse(service.isActive) + + // Enable wake lock + service.enableWakeLock() + XCTAssertTrue(service.isActive) + + // Disable wake lock + service.disableWakeLock() + XCTAssertFalse(service.isActive) + } +} diff --git a/TheNoiseClock.xcodeproj/project.pbxproj b/TheNoiseClock.xcodeproj/project.pbxproj index d2e8f1e..de2ea7f 100644 --- a/TheNoiseClock.xcodeproj/project.pbxproj +++ b/TheNoiseClock.xcodeproj/project.pbxproj @@ -6,6 +6,10 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + EA384E832E6F806200CA7D50 /* AudioPlaybackKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA384D3D2E6F554D00CA7D50 /* AudioPlaybackKit */; }; +/* End PBXBuildFile section */ + /* Begin PBXContainerItemProxy section */ EA384B092E6E6B6100CA7D50 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -65,6 +69,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + EA384E832E6F806200CA7D50 /* AudioPlaybackKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +130,7 @@ ); name = TheNoiseClock; packageProductDependencies = ( + EA384D3D2E6F554D00CA7D50 /* AudioPlaybackKit */, ); productName = TheNoiseClock; productReference = EA384AFB2E6E6B6000CA7D50 /* TheNoiseClock.app */; @@ -208,6 +214,9 @@ ); mainGroup = EA384AF22E6E6B6000CA7D50; minimizedProjectReferenceProxies = 1; + packageReferences = ( + EA384D3C2E6F554D00CA7D50 /* XCLocalSwiftPackageReference "AudioPlaybackKit" */, + ); preferredProjectObjectVersion = 77; productRefGroup = EA384AFC2E6E6B6000CA7D50 /* Products */; projectDirPath = ""; @@ -591,6 +600,21 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + EA384D3C2E6F554D00CA7D50 /* XCLocalSwiftPackageReference "AudioPlaybackKit" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = AudioPlaybackKit; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + EA384D3D2E6F554D00CA7D50 /* AudioPlaybackKit */ = { + isa = XCSwiftPackageProductDependency; + package = EA384D3C2E6F554D00CA7D50 /* XCLocalSwiftPackageReference "AudioPlaybackKit" */; + productName = AudioPlaybackKit; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = EA384AF32E6E6B6000CA7D50 /* Project object */; } diff --git a/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist b/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist index 27be183..8bc45f1 100644 --- a/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ TheNoiseClock.xcscheme_^#shared#^_ orderHint - 0 + 1 diff --git a/TheNoiseClock/Core/Constants/AudioConstants.swift b/TheNoiseClock/Core/Constants/AudioConstants.swift deleted file mode 100644 index 5eb9d80..0000000 --- a/TheNoiseClock/Core/Constants/AudioConstants.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// AudioConstants.swift -// TheNoiseClock -// -// Created by Matt Bruce on 9/7/25. -// - -import Foundation -import AVFAudio - -/// Audio-related constants and configuration -enum AudioConstants { - - // MARK: - Audio Session Configuration - enum AudioSession { - static let category = AVAudioSession.Category.playback - static let mode = AVAudioSession.Mode.default - static let options: AVAudioSession.CategoryOptions = [.mixWithOthers] - } - - // MARK: - Playback Settings - enum Playback { - static let numberOfLoops = -1 // Infinite loop - static let prepareToPlay = true - } - - // MARK: - Volume - enum Volume { - static let min: Float = 0.0 - static let max: Float = 1.0 - static let `default`: Float = 0.8 - } -} diff --git a/TheNoiseClock/Models/Sound.swift b/TheNoiseClock/Models/Sound.swift deleted file mode 100644 index 4a8888f..0000000 --- a/TheNoiseClock/Models/Sound.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// Sound.swift -// TheNoiseClock -// -// Created by Matt Bruce on 9/7/25. -// - -import Foundation - -/// Sound data model for audio files -struct Sound: Identifiable, Hashable { - let id: String - let name: String - let fileName: String - let category: String - let description: String - let bundleName: String? // Optional bundle name for organization - - // MARK: - Initialization - init(name: String, fileName: String, category: String, description: String, bundleName: String? = nil) { - self.id = fileName // Use fileName as stable identifier - self.name = name - self.fileName = fileName - self.category = category - self.description = description - self.bundleName = bundleName - } - - // MARK: - Hashable - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } -} diff --git a/TheNoiseClock/ViewModels/ClockViewModel.swift b/TheNoiseClock/ViewModels/ClockViewModel.swift index 8a00441..f1782e2 100644 --- a/TheNoiseClock/ViewModels/ClockViewModel.swift +++ b/TheNoiseClock/ViewModels/ClockViewModel.swift @@ -8,6 +8,7 @@ import Foundation import Combine import Observation +import AudioPlaybackKit import SwiftUI /// ViewModel for clock display and management diff --git a/TheNoiseClock/Views/Alarms/AddAlarmView.swift b/TheNoiseClock/Views/Alarms/AddAlarmView.swift index 680c668..e4e6f05 100644 --- a/TheNoiseClock/Views/Alarms/AddAlarmView.swift +++ b/TheNoiseClock/Views/Alarms/AddAlarmView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import AudioPlaybackKit /// View for creating new alarms with iOS-native style interface struct AddAlarmView: View { diff --git a/TheNoiseClock/Views/Alarms/Components/SoundSelectionView.swift b/TheNoiseClock/Views/Alarms/Components/SoundSelectionView.swift index d7f7de7..a3c3129 100644 --- a/TheNoiseClock/Views/Alarms/Components/SoundSelectionView.swift +++ b/TheNoiseClock/Views/Alarms/Components/SoundSelectionView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import AudioPlaybackKit /// View for selecting alarm sounds with preview functionality struct SoundSelectionView: View { diff --git a/TheNoiseClock/Views/Alarms/EditAlarmView.swift b/TheNoiseClock/Views/Alarms/EditAlarmView.swift index 0dc2856..07989a4 100644 --- a/TheNoiseClock/Views/Alarms/EditAlarmView.swift +++ b/TheNoiseClock/Views/Alarms/EditAlarmView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import AudioPlaybackKit /// View for editing existing alarms struct EditAlarmView: View { diff --git a/TheNoiseClock/Views/Noise/Components/SoundCategoryView.swift b/TheNoiseClock/Views/Noise/Components/SoundCategoryView.swift index 3e341c6..21e91d7 100644 --- a/TheNoiseClock/Views/Noise/Components/SoundCategoryView.swift +++ b/TheNoiseClock/Views/Noise/Components/SoundCategoryView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import AudioPlaybackKit /// Category-based sound selection view with grid layout struct SoundCategoryView: View { diff --git a/TheNoiseClock/Views/Noise/Components/SoundControlView.swift b/TheNoiseClock/Views/Noise/Components/SoundControlView.swift index e37aeb5..0b9db2e 100644 --- a/TheNoiseClock/Views/Noise/Components/SoundControlView.swift +++ b/TheNoiseClock/Views/Noise/Components/SoundControlView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import AudioPlaybackKit /// Component for audio playback controls struct SoundControlView: View { diff --git a/TheNoiseClock/Views/Noise/NoiseView.swift b/TheNoiseClock/Views/Noise/NoiseView.swift index 5cb2159..3565894 100644 --- a/TheNoiseClock/Views/Noise/NoiseView.swift +++ b/TheNoiseClock/Views/Noise/NoiseView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import AudioPlaybackKit /// Main noise/audio player view struct NoiseView: View {