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

This commit is contained in:
Matt Bruce 2025-09-08 16:59:46 -05:00
parent 2904536334
commit 6d3837022d
6 changed files with 170 additions and 101 deletions

View File

@ -1,5 +1,5 @@
//
// NoisePlayer.swift
// SoundPlayer.swift
// AudioPlaybackKit
//
// Created by Matt Bruce on 9/8/25.
@ -8,13 +8,13 @@
import AVFoundation
import Observation
/// Audio playback service for white noise and ambient sounds
/// Audio playback service for sounds and ambient audio
@available(iOS 17.0, tvOS 17.0, *)
@Observable
public class NoisePlayer {
public class SoundPlayer {
// MARK: - Singleton
public static let shared = NoisePlayer()
public static let shared = SoundPlayer()
// MARK: - Properties
private var players: [String: AVAudioPlayer] = [:]

View File

@ -1,5 +1,5 @@
//
// NoiseViewModel.swift
// SoundViewModel.swift
// AudioPlaybackKit
//
// Created by Matt Bruce on 9/8/25.
@ -8,19 +8,19 @@
import Foundation
import Observation
/// ViewModel for noise/audio playback
/// ViewModel for sound/audio playback
@available(iOS 17.0, tvOS 17.0, *)
@Observable
public class NoiseViewModel {
public class SoundViewModel {
// MARK: - Properties
private let noisePlayer: NoisePlayer
private let soundPlayer: SoundPlayer
private let soundConfigurationService: SoundConfigurationService
public var isPreviewing: Bool = false
public var previewSound: Sound?
public var isPlaying: Bool {
noisePlayer.isPlaying
soundPlayer.isPlaying
}
public var availableSounds: [Sound] {
@ -28,18 +28,18 @@ public class NoiseViewModel {
}
// MARK: - Initialization
public init(noisePlayer: NoisePlayer = NoisePlayer.shared, soundConfigurationService: SoundConfigurationService = SoundConfigurationService.shared) {
self.noisePlayer = noisePlayer
public init(soundPlayer: SoundPlayer = SoundPlayer.shared, soundConfigurationService: SoundConfigurationService = SoundConfigurationService.shared) {
self.soundPlayer = soundPlayer
self.soundConfigurationService = soundConfigurationService
}
// MARK: - Public Interface
public func playSound(_ sound: Sound) {
noisePlayer.playSound(sound)
soundPlayer.playSound(sound)
}
public func stopSound() {
noisePlayer.stopSound()
soundPlayer.stopSound()
}
public func selectSound(_ sound: Sound) {
@ -61,7 +61,7 @@ public class NoiseViewModel {
isPreviewing = true
// Play preview (3 seconds)
noisePlayer.playSound(sound)
soundPlayer.playSound(sound)
// Auto-stop preview after 3 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
@ -71,7 +71,7 @@ public class NoiseViewModel {
public func stopPreview() {
if isPreviewing {
noisePlayer.stopSound()
soundPlayer.stopSound()
isPreviewing = false
previewSound = nil
}

229
PRD.md
View File

@ -63,7 +63,7 @@ TheNoiseClock is a SwiftUI-based iOS application that combines a customizable di
- **Multiple sound options**:
- White Noise (`white-noise.mp3`)
- Heavy Rain White Noise (`heavy-rain-white-noise.mp3`)
- Fan White Noise (`fan-white-noise-heater-303207.mp3`)
- Fan White Noise (`fan-white-noise-heater.mp3`)
- **Continuous playback**: Sounds loop indefinitely
- **Advanced sound selection**: Category-based grid with search and preview
- **Simple controls**: Play/Stop button with visual feedback
@ -77,17 +77,18 @@ TheNoiseClock is a SwiftUI-based iOS application that combines a customizable di
- **Wake lock integration**: Prevents device sleep while audio is playing
- **Bluetooth audio support**: Works with AirPods and other Bluetooth audio devices
- **Responsive layout**: Optimized for both portrait and landscape orientations
- **AudioPlaybackKit integration**: Powered by reusable Swift package for audio functionality
### 6. Advanced Alarm System
- **Multiple alarms**: Create and manage unlimited alarms
- **Rich alarm editor**: Full-featured alarm creation and editing interface
- **Time selection**: Wheel-style date picker with optimized font sizing for maximum readability
- **Dynamic alarm sounds**: Configurable alarm sounds loaded from JSON configuration
- **Dynamic alarm sounds**: Configurable alarm sounds loaded from dedicated alarm-sounds.json configuration
- **Sound preview**: Play/stop functionality for testing alarm sounds before selection
- **Sound organization**: Alarm sounds organized in bundles with categories
- **Sound organization**: Alarm sounds organized in dedicated AlarmSounds.bundle with categories
- **Custom labels**: User-defined alarm names and descriptions
- **Repeat schedules**: Set alarms to repeat on specific weekdays or daily
- **Sound selection**: Choose from extensive system sounds with live preview
- **Sound selection**: Choose from extensive alarm sounds with live preview
- **Volume control**: Adjustable alarm volume (0-100%)
- **Vibration settings**: Enable/disable vibration for each alarm
- **Snooze functionality**: Configurable snooze duration (5, 7, 8, 9, 10, 15, 20 minutes)
@ -98,6 +99,7 @@ TheNoiseClock is a SwiftUI-based iOS application that combines a customizable di
- **Alarm management**: Add, edit, delete, and duplicate alarms
- **Next trigger preview**: Shows when the next alarm will fire
- **Responsive time picker**: Font sizes adapt to available space and orientation
- **AlarmSoundService integration**: Dedicated service for alarm-specific sound management
## Advanced Clock Display Features
@ -139,6 +141,40 @@ TheNoiseClock is a SwiftUI-based iOS application that combines a customizable di
## Technical Architecture
### Swift Package Architecture
TheNoiseClock has been refactored to use a modular Swift Package architecture for improved code reusability and maintainability:
#### AudioPlaybackKit Package
- **Purpose**: Reusable audio playback functionality for iOS and tvOS applications
- **Platform Support**: iOS 17.0+ and tvOS 17.0+
- **Core Components**:
- `Sound` model: Generic sound data structure with Codable support and GUID generation
- `SoundConfiguration` models: Configuration structures for sound management
- `SoundConfigurationService`: Generic sound configuration service with JSON loading
- `NoisePlayer`: Audio playback service with background support
- `WakeLockService`: Screen wake lock management
- `NoiseViewModel`: Audio playback state management
- `AudioConstants`: Audio-related constants and configuration
- **Key Features**:
- Generic sound management (no alarm-specific functionality)
- Fatal error handling for missing configuration files
- Platform-conditional compilation for tvOS compatibility
- Modern @Observable state management
- Background audio support with interruption handling
#### Package Integration
- **Local Package**: AudioPlaybackKit is included as a local Swift package dependency
- **Xcode Integration**: Properly configured in project.pbxproj with XCLocalSwiftPackageReference
- **Import Usage**: Main app imports AudioPlaybackKit for audio functionality
- **Separation of Concerns**: Generic audio functionality in package, app-specific logic in main app
#### App-Specific Extensions
- **AlarmSoundService**: Dedicated service in main app for alarm-specific sound management
- **Configuration Separation**: Alarm sounds use separate alarm-sounds.json file
- **Category Constants**: Hardcoded alarm category ID to avoid magic strings
- **Service Pattern**: Extension pattern for app-specific functionality
### Code Organization Principles
**TOP PRIORITY:** The codebase must be built with the following architectural principles from the beginning:
@ -200,8 +236,17 @@ These principles are fundamental to the project's long-term success and must be
- Sound name with volume control
- Vibration settings
- Snooze duration configuration
- **Sound**: Simple struct for noise file management
- **Sound** (AudioPlaybackKit): Generic sound data model with Codable support
- Unique GUID identifier (auto-generated)
- Display name and file name
- Category and description
- Optional bundle name for organization
- Optional isDefault flag for default sound selection
- Custom Codable implementation with GUID generation
- **SoundConfiguration** (AudioPlaybackKit): Configuration structure for sound management
- Array of Sound objects
- Sound categories and audio settings
- JSON-based configuration loading
- **LegacyAlarm**: Backward compatibility struct for old alarm data
### Data Persistence
@ -210,17 +255,21 @@ These principles are fundamental to the project's long-term success and must be
- **Bundle resources**: Audio files stored in app bundle
### Audio System
- **AudioPlaybackKit Package**: Reusable audio functionality in Swift package
- **AVFoundation**: AVAudioPlayer for noise playback
- **@Observable NoisePlayer**: Modern state management with preloading
- **Looping playback**: Infinite loop for ambient sounds
- **Audio session management**: Proper audio session configuration with background support
- **Error handling**: Graceful handling of missing audio files
- **Error handling**: Fatal error handling for missing configuration files
- **AlarmTonePlayer**: Dedicated player for alarm sound previews
- **Background audio**: Continues playback when app is backgrounded
- **Interruption handling**: Automatic resume after phone calls and route changes
- **Wake lock integration**: Prevents device sleep during audio playback
- **Focus mode awareness**: Monitors and respects Focus mode settings
- **Notification compatibility**: Ensures alarms work with Focus modes enabled
- **Configuration separation**: Separate JSON files for ambient sounds (sounds.json) and alarm sounds (alarm-sounds.json)
- **Generic sound management**: AudioPlaybackKit provides generic sound functionality
- **App-specific extensions**: AlarmSoundService handles alarm-specific sound management
### Battery System
- **@Observable BatteryService**: Modern state management for battery monitoring
@ -283,83 +332,95 @@ These principles are fundamental to the project's long-term success and must be
## File Structure and Organization
### Recommended File Organization
Following the separation of concerns principle, the codebase should be organized into focused, single-responsibility files:
Following the separation of concerns principle, the codebase is organized into focused, single-responsibility files with Swift Package integration:
```
TheNoiseClock/
├── App/
│ ├── TheNoiseClockApp.swift # App entry point and configuration
│ └── ContentView.swift # Main tab navigation coordinator
├── Core/
│ ├── Constants/
│ │ ├── AppConstants.swift # App-wide constants and configuration
│ │ ├── UIConstants.swift # UI-specific constants (colors, sizes, etc.)
│ │ └── AudioConstants.swift # Audio-related constants
│ ├── Extensions/
│ │ ├── Color+Extensions.swift # Color utilities and extensions
│ │ ├── Date+Extensions.swift # Date formatting and utilities
│ │ └── View+Extensions.swift # Common view modifiers and responsive utilities
│ └── Utilities/
│ ├── ColorUtils.swift # Color manipulation utilities
│ ├── FontUtils.swift # Font sizing, typography, and customization utilities
│ └── NotificationUtils.swift # Notification helper functions
├── Models/
│ ├── ClockStyle.swift # Clock customization data model
│ ├── Alarm.swift # Alarm data model
│ ├── Sound.swift # Sound data model
│ └── LegacyAlarm.swift # Backward compatibility model
├── ViewModels/
│ ├── ClockViewModel.swift # Clock display logic and state
│ ├── AlarmViewModel.swift # Alarm management logic
│ └── NoiseViewModel.swift # Audio playback state management
├── Views/
│ ├── Clock/
│ │ ├── ClockView.swift # Main clock display view
│ │ ├── ClockSettingsView.swift # Clock customization interface
├── AudioPlaybackKit/ # Swift Package for reusable audio functionality
│ ├── Package.swift # Package configuration (iOS 17.0+, tvOS 17.0+)
│ └── Sources/AudioPlaybackKit/
│ ├── Models/
│ │ ├── Sound.swift # Generic sound data model with Codable support
│ │ └── SoundConfiguration.swift # Sound configuration models and service
│ ├── Services/
│ │ ├── NoisePlayer.swift # Audio playback service
│ │ └── WakeLockService.swift # Screen wake lock management
│ ├── ViewModels/
│ │ └── NoiseViewModel.swift # Audio playback state management
│ └── Constants/
│ └── AudioConstants.swift # Audio-related constants
├── TheNoiseClock/ # Main application
│ ├── App/
│ │ ├── TheNoiseClockApp.swift # App entry point and configuration
│ │ └── ContentView.swift # Main tab navigation coordinator
│ ├── Core/
│ │ ├── Constants/
│ │ │ ├── AppConstants.swift # App-wide constants and configuration
│ │ │ └── UIConstants.swift # UI-specific constants (colors, sizes, etc.)
│ │ ├── Extensions/
│ │ │ ├── Color+Extensions.swift # Color utilities and extensions
│ │ │ ├── Date+Extensions.swift # Date formatting and utilities
│ │ │ └── View+Extensions.swift # Common view modifiers and responsive utilities
│ │ └── Utilities/
│ │ ├── ColorUtils.swift # Color manipulation utilities
│ │ ├── FontUtils.swift # Font sizing, typography, and customization utilities
│ │ └── NotificationUtils.swift # Notification helper functions
│ ├── Models/
│ │ ├── ClockStyle.swift # Clock customization data model
│ │ ├── Alarm.swift # Alarm data model
│ │ └── LegacyAlarm.swift # Backward compatibility model
│ ├── ViewModels/
│ │ ├── ClockViewModel.swift # Clock display logic and state
│ │ └── AlarmViewModel.swift # Alarm management logic
│ ├── Views/
│ │ ├── Clock/
│ │ │ ├── ClockView.swift # Main clock display view
│ │ │ ├── ClockSettingsView.swift # Clock customization interface
│ │ │ └── Components/
│ │ │ ├── TimeDisplayView.swift # Advanced segmented time display with fixed-width digits
│ │ │ ├── BatteryOverlayView.swift # Battery level overlay
│ │ │ ├── DateOverlayView.swift # Date display overlay
│ │ │ └── TopOverlayView.swift # Combined overlay container
│ │ ├── Alarms/
│ │ │ ├── AlarmView.swift # Main alarm management view
│ │ │ ├── AddAlarmView.swift # Alarm creation interface
│ │ │ └── Components/
│ │ │ ├── AlarmRowView.swift # Individual alarm row component
│ │ │ ├── TimePickerSection.swift # Time selection component
│ │ │ ├── TimeUntilAlarmSection.swift # Time calculation display
│ │ │ ├── SoundSelectionView.swift # Sound selection with preview
│ │ │ ├── LabelEditView.swift # Label editing interface
│ │ │ └── SnoozeSelectionView.swift # Snooze duration selection
│ │ └── Noise/
│ │ ├── NoiseView.swift # Main white noise player interface
│ │ └── Components/
│ │ ├── TimeDisplayView.swift # Advanced segmented time display with fixed-width digits
│ │ ├── BatteryOverlayView.swift # Battery level overlay
│ │ ├── DateOverlayView.swift # Date display overlay
│ │ └── TopOverlayView.swift # Combined overlay container
│ ├── Alarms/
│ │ ├── AlarmView.swift # Main alarm management view
│ │ ├── AddAlarmView.swift # Alarm creation interface
│ │ └── Components/
│ │ ├── AlarmRowView.swift # Individual alarm row component
│ │ ├── TimePickerSection.swift # Time selection component
│ │ ├── TimeUntilAlarmSection.swift # Time calculation display
│ │ ├── SoundSelectionView.swift # Sound selection with preview
│ │ ├── LabelEditView.swift # Label editing interface
│ │ └── SnoozeSelectionView.swift # Snooze duration selection
│ └── Noise/
│ ├── NoiseView.swift # Main white noise player interface
│ └── Components/
│ ├── SoundCategoryView.swift # Advanced grid-based sound selection
│ └── SoundControlView.swift # Playback controls component
├── Services/
│ ├── NoisePlayer.swift # Audio playback service with background support
│ ├── AlarmService.swift # Alarm management service with Focus mode integration
│ ├── NotificationService.swift # Notification handling service
│ ├── WakeLockService.swift # Screen wake lock management service
│ ├── FocusModeService.swift # Focus mode integration and notification management
│ └── BatteryService.swift # Battery monitoring and state management service
└── Resources/
├── sounds.json # Sound configuration and definitions
├── Ambient.bundle/ # Ambient sound category
│ └── white-noise.mp3
├── Nature.bundle/ # Nature sound category
│ └── heavy-rain-white-noise.mp3
├── Mechanical.bundle/ # Mechanical sound category
│ └── fan-white-noise-heater.mp3
├── AlarmSounds.bundle/ # Alarm sound category
│ ├── digital-alarm.mp3
│ ├── iphone-alarm.mp3
│ ├── classic-alarm.mp3
│ ├── beep-alarm.mp3
│ ├── siren-alarm.mp3
│ └── voice-wakeup.mp3
└── Assets.xcassets/
└── [Asset catalogs]
│ │ ├── SoundCategoryView.swift # Advanced grid-based sound selection
│ │ └── SoundControlView.swift # Playback controls component
│ ├── Services/
│ │ ├── AlarmService.swift # Alarm management service with Focus mode integration
│ │ ├── NotificationService.swift # Notification handling service
│ │ ├── FocusModeService.swift # Focus mode integration and notification management
│ │ ├── BatteryService.swift # Battery monitoring and state management service
│ │ └── AlarmSoundService.swift # Alarm-specific sound management service
│ └── Resources/
│ ├── sounds.json # Ambient sound configuration and definitions
│ ├── alarm-sounds.json # Alarm sound configuration and definitions
│ ├── Ambient.bundle/ # Ambient sound category
│ │ └── white-noise.mp3
│ ├── Nature.bundle/ # Nature sound category
│ │ └── heavy-rain-white-noise.mp3
│ ├── Mechanical.bundle/ # Mechanical sound category
│ │ └── fan-white-noise-heater.mp3
│ ├── AlarmSounds.bundle/ # Alarm sound category
│ │ ├── digital-alarm.caf
│ │ ├── classic-alarm.caf
│ │ ├── beep-alarm.caf
│ │ ├── siren-alarm.caf
│ │ └── buzzing-alarm.caf
│ └── Assets.xcassets/
│ └── [Asset catalogs]
└── TheNoiseClock.xcodeproj/ # Xcode project with AudioPlaybackKit dependency
└── project.pbxproj # Project configuration with local package reference
```
### File Naming Conventions
@ -464,6 +525,12 @@ The following changes **automatically require** PRD updates:
- **iOS 26 Features**: Latest platform capabilities where available
### Dependencies
- **AudioPlaybackKit**: Local Swift package for reusable audio functionality
- iOS 17.0+ and tvOS 17.0+ support
- Generic sound management and configuration
- Audio playback with background support
- Wake lock management
- Modern @Observable state management
- **SwiftUI**: Native iOS UI framework with latest features
- **AVFoundation**: Audio playback with modern async patterns and background support
- **UserNotifications**: Alarm notifications with rich content support
@ -514,11 +581,13 @@ The following changes **automatically require** PRD updates:
### Project Information
- **Created**: September 7, 2025
- **Framework**: SwiftUI with iOS 18.0+ target (latest stable features)
- **Architecture**: Modern SwiftUI with @Observable pattern and MVVM
- **Architecture**: Modern SwiftUI with @Observable pattern, MVVM, and Swift Package modularity
- **Package Architecture**: AudioPlaybackKit Swift package for reusable audio functionality
- **Testing**: Comprehensive unit and UI test targets with Swift Testing
- **Version control**: Git repository with feature branch workflow
- **Performance**: Optimized for battery life and smooth operation
- **Modern iOS**: Uses latest iOS 18+ and iOS 26 features with Swift 6 language improvements
- **Code Reusability**: Modular Swift package architecture enables code reuse across projects
### Modern iOS Development Practices
- **Swift 6**: Latest language features including strict concurrency checking

View File

@ -14,7 +14,7 @@ struct SoundSelectionView: View {
@Environment(\.dismiss) private var dismiss
// Use shared player instance to avoid audio conflicts
private let noisePlayer = NoisePlayer.shared
private let soundPlayer = SoundPlayer.shared
private let alarmSounds = AlarmSoundService.shared.getAlarmSounds().sorted { $0.name < $1.name }
@State private var isPlaying = false
@ -73,13 +73,13 @@ struct SoundSelectionView: View {
stopSound()
// Start playing the new sound
noisePlayer.playSound(sound)
soundPlayer.playSound(sound)
isPlaying = true
currentlyPlayingSound = selectedSound
}
private func stopSound() {
noisePlayer.stopSound()
soundPlayer.stopSound()
isPlaying = false
currentlyPlayingSound = nil
}

View File

@ -16,7 +16,7 @@ struct SoundCategoryView: View {
@Binding var selectedSound: Sound?
@State private var selectedCategory: String = "all"
@State private var searchText: String = ""
@State private var viewModel = NoiseViewModel()
@State private var viewModel = SoundViewModel()
// MARK: - Computed Properties
private var filteredSounds: [Sound] {

View File

@ -12,7 +12,7 @@ import AudioPlaybackKit
struct NoiseView: View {
// MARK: - Properties
@State private var viewModel = NoiseViewModel()
@State private var viewModel = SoundViewModel()
@State private var selectedSound: Sound? {
didSet {
// Stop current playback when selecting a new sound