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 {