Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
bf489878dd
commit
b59fe7fe26
137
.gitignore
vendored
Normal file
137
.gitignore
vendored
Normal 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
|
||||
32
AudioPlaybackKit/Package.swift
Normal file
32
AudioPlaybackKit/Package.swift
Normal 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
158
AudioPlaybackKit/README.md
Normal 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.
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
33
AudioPlaybackKit/Sources/AudioPlaybackKit/Models/Sound.swift
Normal file
33
AudioPlaybackKit/Sources/AudioPlaybackKit/Models/Sound.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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 */;
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<key>TheNoiseClock.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
import Observation
|
||||
import AudioPlaybackKit
|
||||
import SwiftUI
|
||||
|
||||
/// ViewModel for clock display and management
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AudioPlaybackKit
|
||||
|
||||
/// View for creating new alarms with iOS-native style interface
|
||||
struct AddAlarmView: View {
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AudioPlaybackKit
|
||||
|
||||
/// View for selecting alarm sounds with preview functionality
|
||||
struct SoundSelectionView: View {
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AudioPlaybackKit
|
||||
|
||||
/// View for editing existing alarms
|
||||
struct EditAlarmView: View {
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AudioPlaybackKit
|
||||
|
||||
/// Category-based sound selection view with grid layout
|
||||
struct SoundCategoryView: View {
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AudioPlaybackKit
|
||||
|
||||
/// Component for audio playback controls
|
||||
struct SoundControlView: View {
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AudioPlaybackKit
|
||||
|
||||
/// Main noise/audio player view
|
||||
struct NoiseView: View {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user