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

View File

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

View File

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

View File

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

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;
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 */;
}

View File

@ -7,7 +7,7 @@
<key>TheNoiseClock.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
<integer>1</integer>
</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 Combine
import Observation
import AudioPlaybackKit
import SwiftUI
/// ViewModel for clock display and management

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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