diff --git a/AudioPlaybackKit/Package.swift b/AudioPlaybackKit/Package.swift
deleted file mode 100644
index a3e2aee..0000000
--- a/AudioPlaybackKit/Package.swift
+++ /dev/null
@@ -1,33 +0,0 @@
-// 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),
- .tvOS(.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"]),
- ]
-)
diff --git a/AudioPlaybackKit/README.md b/AudioPlaybackKit/README.md
deleted file mode 100644
index 71bcdd3..0000000
--- a/AudioPlaybackKit/README.md
+++ /dev/null
@@ -1,158 +0,0 @@
-# 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.
diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/Constants/AudioConstants.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Constants/AudioConstants.swift
deleted file mode 100644
index d687a55..0000000
--- a/AudioPlaybackKit/Sources/AudioPlaybackKit/Constants/AudioConstants.swift
+++ /dev/null
@@ -1,33 +0,0 @@
-//
-// 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
- }
-}
diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/Sound.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/Sound.swift
deleted file mode 100644
index 9568ca2..0000000
--- a/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/Sound.swift
+++ /dev/null
@@ -1,67 +0,0 @@
-//
-// Sound.swift
-// AudioPlaybackKit
-//
-// Created by Matt Bruce on 9/8/25.
-//
-
-import Foundation
-
-/// Sound data model for audio files
-public struct Sound: Identifiable, Hashable, Codable {
- 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 let isDefault: Bool? // Optional - used for alarm sounds to mark default
-
- // MARK: - Initialization
- public init(id: String? = nil, name: String, fileName: String, category: String, description: String, bundleName: String? = nil, isDefault: Bool? = nil) {
- self.id = id ?? UUID().uuidString // Use provided id or generate GUID
- self.name = name
- self.fileName = fileName
- self.category = category
- self.description = description
- self.bundleName = bundleName
- self.isDefault = isDefault
- }
-
- // MARK: - Codable Custom Implementation
- public init(from decoder: Decoder) throws {
- let container = try decoder.container(keyedBy: CodingKeys.self)
-
- // Generate a new GUID for each sound loaded from JSON
- self.id = UUID().uuidString
-
- self.name = try container.decode(String.self, forKey: .name)
- self.fileName = try container.decode(String.self, forKey: .fileName)
- self.category = try container.decode(String.self, forKey: .category)
- self.description = try container.decode(String.self, forKey: .description)
- self.bundleName = try container.decodeIfPresent(String.self, forKey: .bundleName)
- self.isDefault = try container.decodeIfPresent(Bool.self, forKey: .isDefault)
- }
-
- public func encode(to encoder: Encoder) throws {
- var container = encoder.container(keyedBy: CodingKeys.self)
-
- try container.encode(id, forKey: .id)
- try container.encode(name, forKey: .name)
- try container.encode(fileName, forKey: .fileName)
- try container.encode(category, forKey: .category)
- try container.encode(description, forKey: .description)
- try container.encodeIfPresent(bundleName, forKey: .bundleName)
- try container.encodeIfPresent(isDefault, forKey: .isDefault)
- }
-
- private enum CodingKeys: String, CodingKey {
- case id, name, fileName, category, description, bundleName, isDefault
- }
-
- // MARK: - Hashable
- public func hash(into hasher: inout Hasher) {
- hasher.combine(id)
- }
-
-}
diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/SoundConfiguration.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/SoundConfiguration.swift
deleted file mode 100644
index 71759f4..0000000
--- a/AudioPlaybackKit/Sources/AudioPlaybackKit/Models/SoundConfiguration.swift
+++ /dev/null
@@ -1,155 +0,0 @@
-//
-// SoundConfiguration.swift
-// AudioPlaybackKit
-//
-// Created by Matt Bruce on 9/8/25.
-//
-
-import Foundation
-
-/// Configuration model for sound system loaded from JSON
-public struct SoundConfiguration: Codable {
- public let sounds: [Sound]
- public let settings: AudioSettings
-
- public init(sounds: [Sound], settings: AudioSettings) {
- self.sounds = sounds
- self.settings = settings
- }
-}
-
-/// Simple struct for loading just the sounds array from category JSON files
-public struct SoundsOnly: Codable {
- public let sounds: [Sound]
-
- public init(sounds: [Sound]) {
- self.sounds = sounds
- }
-}
-
-
-
-/// Audio settings configuration
-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
-public class SoundConfigurationService {
- public static let shared = SoundConfigurationService()
-
- private var configuration: SoundConfiguration?
-
- private init() {}
-
-
- /// Load audio settings from SoundsSettings.json
- private func loadAudioSettings(from bundle: Bundle = .main) -> AudioSettings {
- guard let url = bundle.url(forResource: "SoundsSettings", withExtension: "json") else {
- print("⚠️ Warning: SoundsSettings.json not found, using default settings")
- return AudioSettings(
- defaultVolume: 0.8,
- defaultLoopCount: -1,
- preloadSounds: true,
- preloadStrategy: "category",
- audioSessionCategory: "playback",
- audioSessionMode: "default",
- audioSessionOptions: ["mixWithOthers"]
- )
- }
-
- do {
- let data = try Data(contentsOf: url)
- let settings = try JSONDecoder().decode(AudioSettings.self, from: data)
- print("✅ Loaded audio settings from SoundsSettings.json")
- return settings
- } catch {
- print("⚠️ Warning: Error loading audio settings, using defaults: \(error)")
- return AudioSettings(
- defaultVolume: 0.8,
- defaultLoopCount: -1,
- preloadSounds: true,
- preloadStrategy: "category",
- audioSessionCategory: "playback",
- audioSessionMode: "default",
- audioSessionOptions: ["mixWithOthers"]
- )
- }
- }
-
- /// Load sound configuration from multiple category-specific JSON files
- public func loadConfigurationFromBundles(from bundle: Bundle = .main) -> SoundConfiguration {
- // Include AlarmSounds bundle for alarm sound preview functionality
- let bundleNames = ["Colored", "Nature", "Mechanical", "Ambient", "AlarmSounds"]
- var allSounds: [Sound] = []
-
- for bundleName in bundleNames {
- guard let bundleURL = bundle.url(forResource: bundleName, withExtension: "bundle"),
- let categoryBundle = Bundle(url: bundleURL),
- let url = categoryBundle.url(forResource: "sounds", withExtension: "json") else {
- print("⚠️ Warning: Could not find sounds.json in \(bundleName).bundle")
- continue
- }
-
- do {
- let data = try Data(contentsOf: url)
- let soundsOnly = try JSONDecoder().decode(SoundsOnly.self, from: data)
- allSounds.append(contentsOf: soundsOnly.sounds)
-
- print("✅ Loaded \(soundsOnly.sounds.count) sounds from \(bundleName).bundle")
- } catch {
- print("⚠️ Warning: Error loading sounds from \(bundleName).bundle: \(error)")
- }
- }
-
- // Load settings from separate file
- let settings = loadAudioSettings(from: bundle)
-
- let config = SoundConfiguration(sounds: allSounds, settings: settings)
- self.configuration = config
- print("✅ Loaded combined sound configuration with \(allSounds.count) sounds from \(bundleNames.count) bundles")
- return config
- }
-
- /// Get current configuration
- public func getConfiguration() -> SoundConfiguration {
- if configuration == nil {
- return loadConfigurationFromBundles()
- }
- return configuration!
- }
-
- /// Get all available sounds
- public func getAvailableSounds() -> [Sound] {
- return getConfiguration().sounds
- }
-
- /// Get sounds by category
- public func getSoundsByCategory(_ categoryId: String) -> [Sound] {
- return getConfiguration().sounds
- .filter { $0.category == categoryId }
- }
-
-
- /// Get audio settings
- public func getAudioSettings() -> AudioSettings {
- return getConfiguration().settings
- }
-
-}
diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/SoundPlayer.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/SoundPlayer.swift
deleted file mode 100644
index 9ab4820..0000000
--- a/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/SoundPlayer.swift
+++ /dev/null
@@ -1,321 +0,0 @@
-//
-// SoundPlayer.swift
-// AudioPlaybackKit
-//
-// Created by Matt Bruce on 9/8/25.
-//
-
-import AVFoundation
-import Observation
-
-/// Audio playback service for sounds and ambient audio
-@available(iOS 17.0, tvOS 17.0, *)
-@Observable
-public class SoundPlayer {
-
- // MARK: - Singleton
- public static let shared = SoundPlayer()
-
- // MARK: - Properties
- private var players: [String: AVAudioPlayer] = [:]
- private let playersLock = NSLock()
- private var currentPlayer: AVAudioPlayer?
- public private(set) var currentSound: Sound?
- private var shouldResumeAfterInterruption = false
- private let wakeLockService = WakeLockService.shared
- private let soundConfigurationService = SoundConfigurationService.shared
-
- // MARK: - Initialization
- private init() {
- setupAudioSession()
- setupAudioInterruptionHandling()
- // Preload sounds off the main thread to avoid blocking UI during app launch
- DispatchQueue.global(qos: .userInitiated).async { [weak self] in
- self?.preloadSounds()
- }
- }
-
- deinit {
- stopAllSounds()
- }
-
- // MARK: - Public Interface
- public var isPlaying: Bool {
- return currentPlayer?.isPlaying ?? false
- }
-
- public func playSound(_ sound: Sound) {
- playSound(sound, volumeOverride: nil)
- }
-
- public func playSound(_ sound: Sound, volume: Float) {
- playSound(sound, volumeOverride: volume)
- }
-
- private func playSound(_ sound: Sound, volumeOverride: Float?) {
- print("🎵 Attempting to play: \(sound.name)")
-
- // Stop current sound if playing
- stopSound()
-
- // Store current sound for interruption handling
- currentSound = sound
-
- // Get or create player for this sound
- playersLock.lock()
- let player = players[sound.fileName]
- playersLock.unlock()
-
- guard let player else {
- playersLock.lock()
- let availableKeys = Array(players.keys)
- playersLock.unlock()
- print("❌ Sound not preloaded: \(sound.fileName)")
- print("📁 Available sounds: \(availableKeys)")
-
- // Try to load the sound dynamically as fallback
- guard let fileUrl = getURL(for: sound) else {
- print("❌ Sound file not found: \(sound.fileName)")
- return
- }
-
- do {
- let newPlayer = try AVAudioPlayer(contentsOf: fileUrl)
- newPlayer.numberOfLoops = AudioConstants.Playback.numberOfLoops
- newPlayer.volume = volumeOverride ?? AudioConstants.Volume.default
- newPlayer.prepareToPlay()
- playersLock.lock()
- players[sound.fileName] = newPlayer
- playersLock.unlock()
- currentPlayer = newPlayer
- let success = newPlayer.play()
- print("🎵 Fallback play result: \(success ? "SUCCESS" : "FAILED")")
- return
- } catch {
- print("❌ Error creating fallback player: \(error)")
- return
- }
- }
-
- currentPlayer = player
- if let volumeOverride {
- player.volume = volumeOverride
- }
- let success = player.play()
- print("🎵 Play result: \(success ? "SUCCESS" : "FAILED")")
- print("🔊 Player isPlaying: \(player.isPlaying)")
- print("🔊 Player volume: \(player.volume)")
-
- // Enable wake lock when playing audio to prevent device sleep
- if success {
- wakeLockService.enableWakeLock()
- }
- }
-
- public func stopSound() {
- currentPlayer?.stop()
- currentPlayer = nil
- shouldResumeAfterInterruption = false
-
- // Disable wake lock when stopping audio
- wakeLockService.disableWakeLock()
- }
-
- public func clearCurrentSound() {
- stopSound()
- currentSound = nil
- }
-
- // MARK: - Private Methods
-
- /// Helper method to get URL for sound file, handling bundles and direct paths
- private func getURL(for sound: Sound) -> URL? {
- // If sound has a bundle name, look in that bundle first
- if let bundleName = sound.bundleName {
- if let bundleURL = Bundle.main.url(forResource: bundleName, withExtension: "bundle"),
- let bundle = Bundle(url: bundleURL) {
- return bundle.url(forResource: sound.fileName, withExtension: nil)
- }
- }
-
- // Fallback to direct file path
- if sound.fileName.contains("/") {
- // Path includes subfolder (e.g., "Sounds/white-noise.mp3")
- let components = sound.fileName.components(separatedBy: "/")
- let fileName = components.last!
- let subfolder = components.dropLast().joined(separator: "/")
- return Bundle.main.url(forResource: fileName, withExtension: nil, subdirectory: subfolder)
- } else {
- // Direct file path (fallback)
- if let url = Bundle.main.url(forResource: sound.fileName, withExtension: nil) {
- return url
- }
- // Alarm sounds live in a subdirectory; try that next
- return Bundle.main.url(forResource: sound.fileName, withExtension: nil, subdirectory: "AlarmSounds")
- }
- }
-
- private func setupAudioSession() {
- do {
- let settings = soundConfigurationService.getAudioSettings()
-
- // Use configuration settings or fall back to constants
- let category = settings.audioSessionCategory == "playback" ?
- AVAudioSession.Category.playback : AudioConstants.AudioSession.category
- let mode = settings.audioSessionMode == "default" ?
- AVAudioSession.Mode.default : AudioConstants.AudioSession.mode
- let options: AVAudioSession.CategoryOptions = settings.audioSessionOptions.contains("mixWithOthers") == true ?
- [.mixWithOthers] : AudioConstants.AudioSession.options
-
- try AVAudioSession.sharedInstance().setCategory(category, mode: mode, options: options)
- try AVAudioSession.sharedInstance().setActive(true)
-
- // Configure for background audio playback
- try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers])
- try AVAudioSession.sharedInstance().setActive(true)
-
- print("🔊 Audio session configured for background playback")
- } catch {
- print("Error setting up audio session: \(error)")
- }
- }
-
- private func preloadSounds() {
- print("📁 Preloading audio files...")
-
- // Get sound configuration
- let sounds = soundConfigurationService.getAvailableSounds()
- let settings = soundConfigurationService.getAudioSettings()
-
- for sound in sounds {
- guard let fileUrl = getURL(for: sound) else {
- print("❌ Sound file not found: \(sound.fileName)")
- continue
- }
-
- do {
- let player = try AVAudioPlayer(contentsOf: fileUrl)
- player.numberOfLoops = settings.defaultLoopCount
- player.volume = settings.defaultVolume
- if settings.preloadSounds {
- player.prepareToPlay()
- }
- playersLock.lock()
- players[sound.fileName] = player
- playersLock.unlock()
- print("✅ Loaded: \(sound.name) (\(sound.fileName))")
- } catch {
- print("❌ Error preloading sound \(sound.fileName): \(error)")
- }
- }
- playersLock.lock()
- let count = players.count
- playersLock.unlock()
- print("📁 Preloading complete. Loaded \(count) sounds.")
- }
-
- private func stopAllSounds() {
- playersLock.lock()
- for player in players.values {
- player.stop()
- }
- players.removeAll()
- playersLock.unlock()
- currentPlayer = nil
- currentSound = nil
- shouldResumeAfterInterruption = false
- }
-
- /// Set up audio interruption handling to maintain playback
- private func setupAudioInterruptionHandling() {
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(handleAudioInterruption),
- name: AVAudioSession.interruptionNotification,
- object: nil
- )
-
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(handleRouteChange),
- name: AVAudioSession.routeChangeNotification,
- object: nil
- )
- }
-
- @objc private func handleAudioInterruption(notification: Notification) {
- guard let userInfo = notification.userInfo,
- let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
- let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
- return
- }
-
- switch type {
- case .began:
- // Audio was interrupted (e.g., phone call)
- shouldResumeAfterInterruption = isPlaying
- if isPlaying {
- currentPlayer?.pause()
- print("🔇 Audio interrupted - will resume after interruption ends")
- }
-
- case .ended:
- // Audio interruption ended
- if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
- let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
- if options.contains(.shouldResume) && shouldResumeAfterInterruption {
- // Resume playback
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
- self.resumePlayback()
- }
- }
- }
-
- @unknown default:
- break
- }
- }
-
- @objc private func handleRouteChange(notification: Notification) {
- guard let userInfo = notification.userInfo,
- let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
- let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
- return
- }
-
- switch reason {
- case .oldDeviceUnavailable:
- // Headphones were unplugged, etc.
- if isPlaying {
- shouldResumeAfterInterruption = true
- currentPlayer?.pause()
- print("🔇 Audio route changed - will resume when new route available")
- }
-
- case .newDeviceAvailable:
- // New audio device available
- if shouldResumeAfterInterruption {
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
- self.resumePlayback()
- }
- }
-
- default:
- break
- }
- }
-
- /// Resume playback after interruption
- private func resumePlayback() {
- guard shouldResumeAfterInterruption, let sound = currentSound else { return }
-
- do {
- try AVAudioSession.sharedInstance().setActive(true)
- playSound(sound)
- shouldResumeAfterInterruption = false
- print("🔊 Audio playback resumed after interruption")
- } catch {
- print("❌ Error resuming audio playback: \(error)")
- }
- }
-}
diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/WakeLockService.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/WakeLockService.swift
deleted file mode 100644
index 0e45408..0000000
--- a/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/WakeLockService.swift
+++ /dev/null
@@ -1,86 +0,0 @@
-//
-// WakeLockService.swift
-// AudioPlaybackKit
-//
-// Created by Matt Bruce on 9/8/25.
-//
-
-#if canImport(UIKit)
-import UIKit
-#endif
-import Observation
-
-/// Service to manage screen wake lock and prevent device from sleeping
-@available(iOS 17.0, tvOS 17.0, *)
-@Observable
-public class WakeLockService {
-
- // MARK: - Singleton
- public static let shared = WakeLockService()
-
- // MARK: - Properties
- public private(set) var isWakeLockActive = false
- private var wakeLockTimer: Timer?
-
- // MARK: - Initialization
- private init() {}
-
- deinit {
- disableWakeLock()
- }
-
- // MARK: - Public Interface
-
- /// Enable wake lock to prevent device from sleeping
- public func enableWakeLock() {
- guard !isWakeLockActive else { return }
-
- #if canImport(UIKit)
- // Prevent device from sleeping
- UIApplication.shared.isIdleTimerDisabled = true
-
- // Set up periodic timer to maintain wake lock
- wakeLockTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { _ in
- // Keep the app active by briefly enabling/disabling idle timer
- UIApplication.shared.isIdleTimerDisabled = false
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
- UIApplication.shared.isIdleTimerDisabled = true
- }
- }
- #endif
-
- isWakeLockActive = true
- print("🔒 Wake lock enabled - device will not sleep")
- }
-
- /// Disable wake lock and allow device to sleep normally
- public func disableWakeLock() {
- guard isWakeLockActive else { return }
-
- #if canImport(UIKit)
- // Allow device to sleep normally
- UIApplication.shared.isIdleTimerDisabled = false
- #endif
-
- // Stop the maintenance timer
- wakeLockTimer?.invalidate()
- wakeLockTimer = nil
-
- isWakeLockActive = false
- print("🔓 Wake lock disabled - device can sleep normally")
- }
-
- /// Toggle wake lock state
- public func toggleWakeLock() {
- if isWakeLockActive {
- disableWakeLock()
- } else {
- enableWakeLock()
- }
- }
-
- /// Check if wake lock is currently active
- public var isActive: Bool {
- return isWakeLockActive
- }
-}
diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/ViewModels/SoundViewModel.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/ViewModels/SoundViewModel.swift
deleted file mode 100644
index 1c1528e..0000000
--- a/AudioPlaybackKit/Sources/AudioPlaybackKit/ViewModels/SoundViewModel.swift
+++ /dev/null
@@ -1,83 +0,0 @@
-//
-// SoundViewModel.swift
-// AudioPlaybackKit
-//
-// Created by Matt Bruce on 9/8/25.
-//
-
-import Foundation
-import Observation
-
-/// ViewModel for sound/audio playback
-@available(iOS 17.0, tvOS 17.0, *)
-@Observable
-public class SoundViewModel {
-
- // MARK: - Properties
- private let soundPlayer: SoundPlayer
- private let soundConfigurationService: SoundConfigurationService
- public var isPreviewing: Bool = false
- public var previewSound: Sound?
-
- public var selectedSound: Sound?
-
- public var isPlaying: Bool {
- soundPlayer.isPlaying
- }
-
- public var availableSounds: [Sound] {
- return soundConfigurationService.getAvailableSounds()
- }
-
- // MARK: - Initialization
- public init(soundPlayer: SoundPlayer = SoundPlayer.shared, soundConfigurationService: SoundConfigurationService = SoundConfigurationService.shared) {
- self.soundPlayer = soundPlayer
- self.soundConfigurationService = soundConfigurationService
- }
-
- // MARK: - Public Interface
- public func playSound(_ sound: Sound) {
- soundPlayer.playSound(sound)
- }
-
- public func stopSound() {
- soundPlayer.stopSound()
- }
-
- public func selectSound(_ sound: Sound) {
- // Stop any current playback when selecting a new sound
- if isPlaying {
- stopSound()
- }
- // Stop any preview
- stopPreview()
-
- selectedSound = sound
- }
-
- // MARK: - Preview Functionality
- public func previewSound(_ sound: Sound) {
- // Stop any current preview
- stopPreview()
-
- // Set preview state
- previewSound = sound
- isPreviewing = true
-
- // Play preview (3 seconds)
- soundPlayer.playSound(sound)
-
- // Auto-stop preview after 3 seconds
- DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
- self.stopPreview()
- }
- }
-
- public func stopPreview() {
- if isPreviewing {
- soundPlayer.stopSound()
- isPreviewing = false
- previewSound = nil
- }
- }
-}
diff --git a/AudioPlaybackKit/Tests/AudioPlaybackKitTests/AudioPlaybackKitTests.swift b/AudioPlaybackKit/Tests/AudioPlaybackKitTests/AudioPlaybackKitTests.swift
deleted file mode 100644
index 8411a81..0000000
--- a/AudioPlaybackKit/Tests/AudioPlaybackKitTests/AudioPlaybackKitTests.swift
+++ /dev/null
@@ -1,48 +0,0 @@
-//
-// 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)
- }
-}
diff --git a/TheNoiseClock.code-workspace b/TheNoiseClock.code-workspace
new file mode 100644
index 0000000..b53107f
--- /dev/null
+++ b/TheNoiseClock.code-workspace
@@ -0,0 +1,20 @@
+{
+ "folders": [
+ {
+ "path": "."
+ },
+ {
+ "path": "../_Packages/Bedrock"
+ },
+ {
+ "path": "../_Packages/AudioPlaybackKit"
+ }
+ ],
+ "settings": {
+ "terminal.integrated.enablePersistentSessions": true,
+ "terminal.integrated.persistentSessionReviveProcess": "onExitAndWindowClose",
+ "task.allowAutomaticTasks": "off",
+ "swift.disableAutoResolve": true,
+ "swift.disableSwiftPackageManagerIntegration": true
+ }
+}
diff --git a/TheNoiseClock.xcodeproj/project.pbxproj b/TheNoiseClock.xcodeproj/project.pbxproj
index 79973b6..6ae4cb3 100644
--- a/TheNoiseClock.xcodeproj/project.pbxproj
+++ b/TheNoiseClock.xcodeproj/project.pbxproj
@@ -7,7 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
- EA384E832E6F806200CA7D50 /* AudioPlaybackKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA384D3D2E6F554D00CA7D50 /* AudioPlaybackKit */; };
+ EA756C592F465C07006196BB /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA756C582F465C07006196BB /* Bedrock */; };
+ EA756C5C2F465C3E006196BB /* AudioPlaybackKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA756C5B2F465C3E006196BB /* AudioPlaybackKit */; };
EAC051B12F2E64AB007F87EA /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EAC051B02F2E64AB007F87EA /* Bedrock */; };
EAF1C0DE2F3A4B5C0011223E /* TheNoiseClockWidget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = EAF1C0DE2F3A4B5C00112234 /* TheNoiseClockWidget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
@@ -113,7 +114,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- EA384E832E6F806200CA7D50 /* AudioPlaybackKit in Frameworks */,
+ EA756C5C2F465C3E006196BB /* AudioPlaybackKit in Frameworks */,
+ EA756C592F465C07006196BB /* Bedrock in Frameworks */,
EAC051B12F2E64AB007F87EA /* Bedrock in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -196,8 +198,9 @@
);
name = TheNoiseClock;
packageProductDependencies = (
- EA384D3D2E6F554D00CA7D50 /* AudioPlaybackKit */,
EAC051B02F2E64AB007F87EA /* Bedrock */,
+ EA756C582F465C07006196BB /* Bedrock */,
+ EA756C5B2F465C3E006196BB /* AudioPlaybackKit */,
);
productName = TheNoiseClock;
productReference = EA384AFB2E6E6B6000CA7D50 /* The Noise Clock.app */;
@@ -305,8 +308,8 @@
mainGroup = EA384AF22E6E6B6000CA7D50;
minimizedProjectReferenceProxies = 1;
packageReferences = (
- EA384D3C2E6F554D00CA7D50 /* XCLocalSwiftPackageReference "AudioPlaybackKit" */,
- EAC051AF2F2E64AB007F87EA /* XCLocalSwiftPackageReference "../Bedrock" */,
+ EA756C572F465C07006196BB /* XCLocalSwiftPackageReference "../_Packages/Bedrock" */,
+ EA756C5A2F465C3E006196BB /* XCLocalSwiftPackageReference "../_Packages/AudioPlaybackKit" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = EA384AFC2E6E6B6000CA7D50 /* Products */;
@@ -438,7 +441,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
- DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
+ DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -504,7 +507,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
+ DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -772,20 +775,23 @@
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
- EA384D3C2E6F554D00CA7D50 /* XCLocalSwiftPackageReference "AudioPlaybackKit" */ = {
+ EA756C572F465C07006196BB /* XCLocalSwiftPackageReference "../_Packages/Bedrock" */ = {
isa = XCLocalSwiftPackageReference;
- relativePath = AudioPlaybackKit;
+ relativePath = ../_Packages/Bedrock;
};
- EAC051AF2F2E64AB007F87EA /* XCLocalSwiftPackageReference "../Bedrock" */ = {
+ EA756C5A2F465C3E006196BB /* XCLocalSwiftPackageReference "../_Packages/AudioPlaybackKit" */ = {
isa = XCLocalSwiftPackageReference;
- relativePath = ../Bedrock;
+ relativePath = ../_Packages/AudioPlaybackKit;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
- EA384D3D2E6F554D00CA7D50 /* AudioPlaybackKit */ = {
+ EA756C582F465C07006196BB /* Bedrock */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = Bedrock;
+ };
+ EA756C5B2F465C3E006196BB /* AudioPlaybackKit */ = {
isa = XCSwiftPackageProductDependency;
- package = EA384D3C2E6F554D00CA7D50 /* XCLocalSwiftPackageReference "AudioPlaybackKit" */;
productName = AudioPlaybackKit;
};
EAC051B02F2E64AB007F87EA /* Bedrock */ = {
diff --git a/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist b/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist
index 528733b..1bbd017 100644
--- a/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -7,12 +7,12 @@
TheNoiseClock.xcscheme_^#shared#^_
orderHint
- 2
+ 0
TheNoiseClockWidget.xcscheme_^#shared#^_
orderHint
- 3
+ 1
diff --git a/TheNoiseClock/Localizable.xcstrings b/TheNoiseClock/Localizable.xcstrings
index 279fd32..4b6103f 100644
--- a/TheNoiseClock/Localizable.xcstrings
+++ b/TheNoiseClock/Localizable.xcstrings
@@ -3454,7 +3454,6 @@
}
},
"settings.debug.branding_preview.subtitle" : {
- "extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -3477,7 +3476,6 @@
}
},
"settings.debug.branding_preview.title" : {
- "extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -3500,7 +3498,6 @@
}
},
"settings.debug.icon_generator.subtitle" : {
- "extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -3523,7 +3520,6 @@
}
},
"settings.debug.icon_generator.title" : {
- "extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -3546,7 +3542,6 @@
}
},
"settings.debug.reset_onboarding.subtitle" : {
- "extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -3569,7 +3564,6 @@
}
},
"settings.debug.reset_onboarding.title" : {
- "extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@@ -3592,7 +3586,6 @@
}
},
"settings.debug.section_title" : {
- "extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {