Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2025-09-08 16:29:47 -05:00
parent bf489878dd
commit b59fe7fe26
21 changed files with 585 additions and 151 deletions

137
.gitignore vendored Normal file
View File

@ -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

View File

@ -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"]),
]
)

158
AudioPlaybackKit/README.md Normal file
View File

@ -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.

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -1,65 +1,97 @@
// //
// SoundConfiguration.swift // SoundConfiguration.swift
// TheNoiseClock // AudioPlaybackKit
// //
// Created by Matt Bruce on 9/7/25. // Created by Matt Bruce on 9/8/25.
// //
import Foundation import Foundation
/// Configuration model for sound system loaded from JSON /// Configuration model for sound system loaded from JSON
struct SoundConfiguration: Codable { public struct SoundConfiguration: Codable {
let sounds: [SoundConfig] public let sounds: [SoundConfig]
let categories: [SoundCategory] public let categories: [SoundCategory]
let settings: AudioSettings public let settings: AudioSettings
public init(sounds: [SoundConfig], categories: [SoundCategory], settings: AudioSettings) {
self.sounds = sounds
self.categories = categories
self.settings = settings
}
} }
/// Individual sound configuration /// Individual sound configuration
struct SoundConfig: Codable, Identifiable { public struct SoundConfig: Codable, Identifiable {
let id: String public let id: String
let name: String public let name: String
let fileName: String public let fileName: String
let category: String public let category: String
let description: String public let description: String
let bundleName: String? // Optional bundle name for organization 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 /// 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) return Sound(name: name, fileName: fileName, category: category, description: description, bundleName: bundleName)
} }
} }
/// Sound category configuration /// Sound category configuration
struct SoundCategory: Codable, Identifiable { public struct SoundCategory: Codable, Identifiable {
let id: String public let id: String
let name: String public let name: String
let description: String public let description: String
let bundleName: String? // Optional bundle name for this category 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 /// Audio settings configuration
struct AudioSettings: Codable { public struct AudioSettings: Codable {
let defaultVolume: Float public let defaultVolume: Float
let defaultLoopCount: Int public let defaultLoopCount: Int
let preloadSounds: Bool public let preloadSounds: Bool
let preloadStrategy: String // "all", "category", "none" public let preloadStrategy: String // "all", "category", "none"
let audioSessionCategory: String public let audioSessionCategory: String
let audioSessionMode: String public let audioSessionMode: String
let audioSessionOptions: [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 /// Service for loading and managing sound configuration
class SoundConfigurationService { public class SoundConfigurationService {
static let shared = SoundConfigurationService() public static let shared = SoundConfigurationService()
private var configuration: SoundConfiguration? private var configuration: SoundConfiguration?
private init() {} private init() {}
/// Load sound configuration from JSON file /// Load sound configuration from JSON file
func loadConfiguration() -> SoundConfiguration? { public func loadConfiguration(from bundle: Bundle = .main, fileName: String = "sounds") -> SoundConfiguration? {
guard let url = Bundle.main.url(forResource: "sounds", withExtension: "json") else { guard let url = bundle.url(forResource: fileName, withExtension: "json") else {
print("sounds.json not found in bundle") print("\(fileName).json not found in bundle")
return nil return nil
} }
@ -76,7 +108,7 @@ class SoundConfigurationService {
} }
/// Get current configuration /// Get current configuration
func getConfiguration() -> SoundConfiguration? { public func getConfiguration() -> SoundConfiguration? {
if configuration == nil { if configuration == nil {
return loadConfiguration() return loadConfiguration()
} }
@ -84,7 +116,7 @@ class SoundConfigurationService {
} }
/// Get all available sounds /// Get all available sounds
func getAvailableSounds() -> [Sound] { public func getAvailableSounds() -> [Sound] {
guard let config = getConfiguration() else { guard let config = getConfiguration() else {
print("⚠️ No configuration available, falling back to constants") print("⚠️ No configuration available, falling back to constants")
return getFallbackSounds() return getFallbackSounds()
@ -94,7 +126,7 @@ class SoundConfigurationService {
} }
/// Get sounds by category /// Get sounds by category
func getSoundsByCategory(_ categoryId: String) -> [Sound] { public func getSoundsByCategory(_ categoryId: String) -> [Sound] {
guard let config = getConfiguration() else { guard let config = getConfiguration() else {
return [] return []
} }
@ -105,7 +137,7 @@ class SoundConfigurationService {
} }
/// Get sounds by bundle name /// Get sounds by bundle name
func getSoundsByBundle(_ bundleName: String) -> [Sound] { public func getSoundsByBundle(_ bundleName: String) -> [Sound] {
guard let config = getConfiguration() else { guard let config = getConfiguration() else {
return [] return []
} }
@ -116,17 +148,17 @@ class SoundConfigurationService {
} }
/// Get alarm sounds specifically /// Get alarm sounds specifically
func getAlarmSounds() -> [Sound] { public func getAlarmSounds() -> [Sound] {
return getSoundsByCategory("alarm") return getSoundsByCategory("alarm")
} }
/// Get available categories /// Get available categories
func getAvailableCategories() -> [SoundCategory] { public func getAvailableCategories() -> [SoundCategory] {
return getConfiguration()?.categories ?? [] return getConfiguration()?.categories ?? []
} }
/// Get audio settings /// Get audio settings
func getAudioSettings() -> AudioSettings? { public func getAudioSettings() -> AudioSettings? {
return getConfiguration()?.settings return getConfiguration()?.settings
} }

View File

@ -1,19 +1,20 @@
// //
// NoisePlayer.swift // NoisePlayer.swift
// TheNoiseClock // AudioPlaybackKit
// //
// Created by Matt Bruce on 9/7/25. // Created by Matt Bruce on 9/8/25.
// //
import AVFoundation import AVFoundation
import Observation import Observation
/// Audio playback service for white noise and ambient sounds /// Audio playback service for white noise and ambient sounds
@available(iOS 17.0, *)
@Observable @Observable
class NoisePlayer { public class NoisePlayer {
// MARK: - Singleton // MARK: - Singleton
static let shared = NoisePlayer() public static let shared = NoisePlayer()
// MARK: - Properties // MARK: - Properties
private var players: [String: AVAudioPlayer] = [:] private var players: [String: AVAudioPlayer] = [:]
@ -21,14 +22,13 @@ class NoisePlayer {
private var currentSound: Sound? private var currentSound: Sound?
private var shouldResumeAfterInterruption = false private var shouldResumeAfterInterruption = false
private let wakeLockService = WakeLockService.shared private let wakeLockService = WakeLockService.shared
private let focusModeService = FocusModeService.shared private let soundConfigurationService = SoundConfigurationService.shared
// MARK: - Initialization // MARK: - Initialization
private init() { private init() {
setupAudioSession() setupAudioSession()
preloadSounds() preloadSounds()
setupAudioInterruptionHandling() setupAudioInterruptionHandling()
focusModeService.configureForFocusModes()
} }
deinit { deinit {
@ -36,21 +36,13 @@ class NoisePlayer {
} }
// MARK: - Public Interface // MARK: - Public Interface
var isPlaying: Bool { public var isPlaying: Bool {
return currentPlayer?.isPlaying ?? false return currentPlayer?.isPlaying ?? false
} }
func playSound(_ sound: Sound) { public func playSound(_ sound: Sound) {
print("🎵 Attempting to play: \(sound.name)") 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 // Stop current sound if playing
stopSound() stopSound()
@ -96,7 +88,7 @@ class NoisePlayer {
} }
} }
func stopSound() { public func stopSound() {
currentPlayer?.stop() currentPlayer?.stop()
currentPlayer = nil currentPlayer = nil
currentSound = nil currentSound = nil
@ -133,7 +125,7 @@ class NoisePlayer {
private func setupAudioSession() { private func setupAudioSession() {
do { do {
let settings = SoundConfigurationService.shared.getAudioSettings() let settings = soundConfigurationService.getAudioSettings()
// Use configuration settings or fall back to constants // Use configuration settings or fall back to constants
let category = settings?.audioSessionCategory == "playback" ? let category = settings?.audioSessionCategory == "playback" ?
@ -147,7 +139,7 @@ class NoisePlayer {
try AVAudioSession.sharedInstance().setActive(true) try AVAudioSession.sharedInstance().setActive(true)
// Configure for background audio playback // 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) try AVAudioSession.sharedInstance().setActive(true)
print("🔊 Audio session configured for background playback") print("🔊 Audio session configured for background playback")
@ -160,8 +152,8 @@ class NoisePlayer {
print("📁 Preloading audio files...") print("📁 Preloading audio files...")
// Get sound configuration // Get sound configuration
let sounds = SoundConfigurationService.shared.getAvailableSounds() let sounds = soundConfigurationService.getAvailableSounds()
let settings = SoundConfigurationService.shared.getAudioSettings() let settings = soundConfigurationService.getAudioSettings()
for sound in sounds { for sound in sounds {
guard let fileUrl = getURL(for: sound) else { guard let fileUrl = getURL(for: sound) else {

View File

@ -1,22 +1,23 @@
// //
// WakeLockService.swift // WakeLockService.swift
// TheNoiseClock // AudioPlaybackKit
// //
// Created by Matt Bruce on 9/7/25. // Created by Matt Bruce on 9/8/25.
// //
import UIKit import UIKit
import Observation import Observation
/// Service to manage screen wake lock and prevent device from sleeping /// Service to manage screen wake lock and prevent device from sleeping
@available(iOS 17.0, *)
@Observable @Observable
class WakeLockService { public class WakeLockService {
// MARK: - Singleton // MARK: - Singleton
static let shared = WakeLockService() public static let shared = WakeLockService()
// MARK: - Properties // MARK: - Properties
private(set) var isWakeLockActive = false public private(set) var isWakeLockActive = false
private var wakeLockTimer: Timer? private var wakeLockTimer: Timer?
// MARK: - Initialization // MARK: - Initialization
@ -29,7 +30,7 @@ class WakeLockService {
// MARK: - Public Interface // MARK: - Public Interface
/// Enable wake lock to prevent device from sleeping /// Enable wake lock to prevent device from sleeping
func enableWakeLock() { public func enableWakeLock() {
guard !isWakeLockActive else { return } guard !isWakeLockActive else { return }
// Prevent device from sleeping // Prevent device from sleeping
@ -49,7 +50,7 @@ class WakeLockService {
} }
/// Disable wake lock and allow device to sleep normally /// Disable wake lock and allow device to sleep normally
func disableWakeLock() { public func disableWakeLock() {
guard isWakeLockActive else { return } guard isWakeLockActive else { return }
// Allow device to sleep normally // Allow device to sleep normally
@ -64,7 +65,7 @@ class WakeLockService {
} }
/// Toggle wake lock state /// Toggle wake lock state
func toggleWakeLock() { public func toggleWakeLock() {
if isWakeLockActive { if isWakeLockActive {
disableWakeLock() disableWakeLock()
} else { } else {
@ -73,7 +74,7 @@ class WakeLockService {
} }
/// Check if wake lock is currently active /// Check if wake lock is currently active
var isActive: Bool { public var isActive: Bool {
return isWakeLockActive return isWakeLockActive
} }
} }

View File

@ -1,45 +1,48 @@
// //
// NoiseViewModel.swift // NoiseViewModel.swift
// TheNoiseClock // AudioPlaybackKit
// //
// Created by Matt Bruce on 9/7/25. // Created by Matt Bruce on 9/8/25.
// //
import Foundation import Foundation
import Observation import Observation
/// ViewModel for noise/audio playback /// ViewModel for noise/audio playback
@available(iOS 17.0, *)
@Observable @Observable
class NoiseViewModel { public class NoiseViewModel {
// MARK: - Properties // MARK: - Properties
private let noisePlayer: NoisePlayer private let noisePlayer: NoisePlayer
var isPreviewing: Bool = false private let soundConfigurationService: SoundConfigurationService
var previewSound: Sound? public var isPreviewing: Bool = false
public var previewSound: Sound?
var isPlaying: Bool { public var isPlaying: Bool {
noisePlayer.isPlaying noisePlayer.isPlaying
} }
var availableSounds: [Sound] { public var availableSounds: [Sound] {
return SoundConfigurationService.shared.getAvailableSounds() return soundConfigurationService.getAvailableSounds()
} }
// MARK: - Initialization // MARK: - Initialization
init(noisePlayer: NoisePlayer = NoisePlayer.shared) { public init(noisePlayer: NoisePlayer = NoisePlayer.shared, soundConfigurationService: SoundConfigurationService = SoundConfigurationService.shared) {
self.noisePlayer = noisePlayer self.noisePlayer = noisePlayer
self.soundConfigurationService = soundConfigurationService
} }
// MARK: - Public Interface // MARK: - Public Interface
func playSound(_ sound: Sound) { public func playSound(_ sound: Sound) {
noisePlayer.playSound(sound) noisePlayer.playSound(sound)
} }
func stopSound() { public func stopSound() {
noisePlayer.stopSound() noisePlayer.stopSound()
} }
func selectSound(_ sound: Sound) { public func selectSound(_ sound: Sound) {
// Stop any current playback when selecting a new sound // Stop any current playback when selecting a new sound
if isPlaying { if isPlaying {
stopSound() stopSound()
@ -49,7 +52,7 @@ class NoiseViewModel {
} }
// MARK: - Preview Functionality // MARK: - Preview Functionality
func previewSound(_ sound: Sound) { public func previewSound(_ sound: Sound) {
// Stop any current preview // Stop any current preview
stopPreview() stopPreview()
@ -66,7 +69,7 @@ class NoiseViewModel {
} }
} }
func stopPreview() { public func stopPreview() {
if isPreviewing { if isPreviewing {
noisePlayer.stopSound() noisePlayer.stopSound()
isPreviewing = false isPreviewing = false

View File

@ -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)
}
}

View File

@ -6,6 +6,10 @@
objectVersion = 77; objectVersion = 77;
objects = { objects = {
/* Begin PBXBuildFile section */
EA384E832E6F806200CA7D50 /* AudioPlaybackKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA384D3D2E6F554D00CA7D50 /* AudioPlaybackKit */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
EA384B092E6E6B6100CA7D50 /* PBXContainerItemProxy */ = { EA384B092E6E6B6100CA7D50 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
@ -65,6 +69,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
EA384E832E6F806200CA7D50 /* AudioPlaybackKit in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -125,6 +130,7 @@
); );
name = TheNoiseClock; name = TheNoiseClock;
packageProductDependencies = ( packageProductDependencies = (
EA384D3D2E6F554D00CA7D50 /* AudioPlaybackKit */,
); );
productName = TheNoiseClock; productName = TheNoiseClock;
productReference = EA384AFB2E6E6B6000CA7D50 /* TheNoiseClock.app */; productReference = EA384AFB2E6E6B6000CA7D50 /* TheNoiseClock.app */;
@ -208,6 +214,9 @@
); );
mainGroup = EA384AF22E6E6B6000CA7D50; mainGroup = EA384AF22E6E6B6000CA7D50;
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
packageReferences = (
EA384D3C2E6F554D00CA7D50 /* XCLocalSwiftPackageReference "AudioPlaybackKit" */,
);
preferredProjectObjectVersion = 77; preferredProjectObjectVersion = 77;
productRefGroup = EA384AFC2E6E6B6000CA7D50 /* Products */; productRefGroup = EA384AFC2E6E6B6000CA7D50 /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -591,6 +600,21 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* 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 */; rootObject = EA384AF32E6E6B6000CA7D50 /* Project object */;
} }

View File

@ -7,7 +7,7 @@
<key>TheNoiseClock.xcscheme_^#shared#^_</key> <key>TheNoiseClock.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>0</integer> <integer>1</integer>
</dict> </dict>
</dict> </dict>
</dict> </dict>

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
import Combine import Combine
import Observation import Observation
import AudioPlaybackKit
import SwiftUI import SwiftUI
/// ViewModel for clock display and management /// ViewModel for clock display and management

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import AudioPlaybackKit
/// View for creating new alarms with iOS-native style interface /// View for creating new alarms with iOS-native style interface
struct AddAlarmView: View { struct AddAlarmView: View {

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import AudioPlaybackKit
/// View for selecting alarm sounds with preview functionality /// View for selecting alarm sounds with preview functionality
struct SoundSelectionView: View { struct SoundSelectionView: View {

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import AudioPlaybackKit
/// View for editing existing alarms /// View for editing existing alarms
struct EditAlarmView: View { struct EditAlarmView: View {

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import AudioPlaybackKit
/// Category-based sound selection view with grid layout /// Category-based sound selection view with grid layout
struct SoundCategoryView: View { struct SoundCategoryView: View {

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import AudioPlaybackKit
/// Component for audio playback controls /// Component for audio playback controls
struct SoundControlView: View { struct SoundControlView: View {

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import AudioPlaybackKit
/// Main noise/audio player view /// Main noise/audio player view
struct NoiseView: View { struct NoiseView: View {