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
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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")
|
Sound(name: "Voice Wake Up", fileName: "voice-wakeup.mp3", category: "alarm", description: "Voice-based wake up sound", bundleName: "AlarmSounds")
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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 {
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
@ -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;
|
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 */;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user